/*-------------------------------------------------------------------------
 *
 * ri_triggers.c
 *
 *	用于参照完整性约束检查的通用触发程序。
 *
 *	关于内存管理的说明：此处保存的私有哈希表
 * 在查询和事务边界之间存在，实际上它们的
 * 生命周期与后端相同。之所以能这样工作，是因为哈希表
 * 结构本身由dynahash.c分配于其持久的DynaHashCxt中，
 * 而它们指向的SPI计划则通过SPI_keepplan()保存。
 * 目前没有任何机制来丢弃不再需要的计划——
 * 考虑在某天改进这一点。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 *
 * src/backend/utils/adt/ri_triggers.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/htup_details.h"
#include "access/sysattr.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "executor/spi.h"
#include "lib/ilist.h"
#include "miscadmin.h"
#include "parser/parse_coerce.h"
#include "parser/parse_relation.h"
#include "storage/bufmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rel.h"
#include "utils/rls.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"

/*
 * 本地定义
 */

#define RI_MAX_NUMKEYS					INDEX_MAX_KEYS

#define RI_INIT_CONSTRAINTHASHSIZE		64
#define RI_INIT_QUERYHASHSIZE			(RI_INIT_CONSTRAINTHASHSIZE * 4)

#define RI_KEYS_ALL_NULL				0
#define RI_KEYS_SOME_NULL				1
#define RI_KEYS_NONE_NULL				2

/* RI查询类型代码 */
/* 这些查询针对 PK（被引用）表执行：*/
#define RI_PLAN_CHECK_LOOKUPPK			1
#define RI_PLAN_CHECK_LOOKUPPK_FROM_PK	2
#define RI_PLAN_LAST_ON_PK				RI_PLAN_CHECK_LOOKUPPK_FROM_PK
/* 这些查询针对 FK（引用）表执行：*/
#define RI_PLAN_CASCADE_ONDELETE		3
#define RI_PLAN_CASCADE_ONUPDATE		4
/* 对于 RESTRICT，ON DELETE 和 ON UPDATE 触发器可以使用相同的计划。*/
#define RI_PLAN_RESTRICT				5
#define RI_PLAN_SETNULL_ONDELETE		6
#define RI_PLAN_SETNULL_ONUPDATE		7
#define RI_PLAN_SETDEFAULT_ONDELETE		8
#define RI_PLAN_SETDEFAULT_ONUPDATE		9

#define MAX_QUOTED_NAME_LEN  (NAMEDATALEN*2+3)
#define MAX_QUOTED_REL_NAME_LEN  (MAX_QUOTED_NAME_LEN*2)

#define RIAttName(rel, attnum)	NameStr(*attnumAttName(rel, attnum))
#define RIAttType(rel, attnum)	attnumTypeId(rel, attnum)
#define RIAttCollation(rel, attnum) attnumCollationId(rel, attnum)

#define RI_TRIGTYPE_INSERT 1
#define RI_TRIGTYPE_UPDATE 2
#define RI_TRIGTYPE_DELETE 3


/*
 * RI_ConstraintInfo
 *
 * 从 FK pg_constraint 条目中提取的信息。这个信息被缓存于
 * ri_constraint_cache。
 */
typedef struct RI_ConstraintInfo
{
	Oid			constraint_id;	/* pg_constraint 条目的 OID（哈希键）*/
	bool		valid;			/* 是否成功初始化？*/
	Oid			constraint_root_id; /* 最上层祖先约束的 OID；
									 * 如果不是继承的，等同于 constraint_id */
	uint32		oidHashValue;	/* constraint_id 的哈希值 */
	uint32		rootHashValue;	/* constraint_root_id 的哈希值 */
	NameData	conname;		/* FK 约束的名称 */
	Oid			pk_relid;		/* 被引用的关系 */
	Oid			fk_relid;		/* 引用的关系 */
	char		confupdtype;	/* 外键的 ON UPDATE 操作 */
	char		confdeltype;	/* 外键的 ON DELETE 操作 */
	int			ndelsetcols;	/* 在 ON DELETE SET 子句中引用的列数 */
	int16		confdelsetcols[RI_MAX_NUMKEYS]; /* 在删除时要设置的列的 attnums */
	char		confmatchtype;	/* 外键的匹配类型 */
	int			nkeys;			/* 关键列的数量 */
	int16		pk_attnums[RI_MAX_NUMKEYS]; /* 被引用列的 attnums */
	int16		fk_attnums[RI_MAX_NUMKEYS]; /* 引用列的 attnums */
	Oid			pf_eq_oprs[RI_MAX_NUMKEYS]; /* 相等运算符（PK = FK） */
	Oid			pp_eq_oprs[RI_MAX_NUMKEYS]; /* 相等运算符（PK = PK） */
	Oid			ff_eq_oprs[RI_MAX_NUMKEYS]; /* 相等运算符（FK = FK） */
	dlist_node	valid_link;		/* 有效条目列表中的链接 */
} RI_ConstraintInfo;

/*
 * RI_QueryKey
 *
 * 用于在我们的查询哈希表中识别已准备好的 SPI 计划的键
 */
typedef struct RI_QueryKey
{
	Oid			constr_id;		/* pg_constraint 条目的 OID */
	int32		constr_queryno; /* 查询类型 ID，请参见上面的 RI_PLAN_XXX */
} RI_QueryKey;

/*
 * RI_QueryHashEntry
 */
typedef struct RI_QueryHashEntry
{
	RI_QueryKey key;
	SPIPlanPtr	plan;
} RI_QueryHashEntry;

/*
 * RI_CompareKey
 *
 * 用于识别展示如何比较两个值的条目的键
 */
typedef struct RI_CompareKey
{
	Oid			eq_opr;			/* 应用的相等运算符 */
	Oid			typeid;			/* 应用到的数据类型 */
} RI_CompareKey;

/*
 * RI_CompareHashEntry
 */
typedef struct RI_CompareHashEntry
{
	RI_CompareKey key;
	bool		valid;			/* 是否成功初始化？*/
	FmgrInfo	eq_opr_finfo;	/* 等式函数的调用信息 */
	FmgrInfo	cast_func_finfo;	/* 以防我们需要强制输入 */
} RI_CompareHashEntry;


/*
 * 本地数据
 */
static HTAB *ri_constraint_cache = NULL;
static HTAB *ri_query_cache = NULL;
static HTAB *ri_compare_cache = NULL;
static dlist_head ri_constraint_cache_valid_list;
static int	ri_constraint_cache_valid_count = 0;


/*
 * 本地函数原型
 */
static bool fc_ri_Check_Pk_Match(Relation fc_pk_rel, Relation fc_fk_rel,
							  TupleTableSlot *fc_oldslot,
							  const RI_ConstraintInfo *fc_riinfo);
static Datum fc_ri_restrict(TriggerData *fc_trigdata, bool fc_is_no_action);
static Datum fc_ri_set(TriggerData *fc_trigdata, bool fc_is_set_null, int fc_tgkind);
static void fc_quoteOneName(char *fc_buffer, const char *fc_name);
static void fc_quoteRelationName(char *fc_buffer, Relation fc_rel);
static void fc_ri_GenerateQual(StringInfo fc_buf,
							const char *fc_sep,
							const char *fc_leftop, Oid fc_leftoptype,
							Oid fc_opoid,
							const char *fc_rightop, Oid fc_rightoptype);
static void fc_ri_GenerateQualCollation(StringInfo fc_buf, Oid fc_collation);
static int	fc_ri_NullCheck(TupleDesc fc_tupdesc, TupleTableSlot *fc_slot,
						 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk);
static void fc_ri_BuildQueryKey(RI_QueryKey *fc_key,
							 const RI_ConstraintInfo *fc_riinfo,
							 int32 fc_constr_queryno);
static bool fc_ri_KeysEqual(Relation fc_rel, TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot,
						 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk);
static bool fc_ri_AttributesEqual(Oid fc_eq_opr, Oid fc_typeid,
							   Datum fc_oldvalue, Datum fc_newvalue);

static void fc_ri_InitHashTables(void);
static void fc_InvalidateConstraintCacheCallBack(Datum fc_arg, int fc_cacheid, uint32 fc_hashvalue);
static SPIPlanPtr fc_ri_FetchPreparedPlan(RI_QueryKey *fc_key);
static void fc_ri_HashPreparedPlan(RI_QueryKey *fc_key, SPIPlanPtr fc_plan);
static RI_CompareHashEntry *fc_ri_HashCompareOp(Oid fc_eq_opr, Oid fc_typeid);

static void fc_ri_CheckTrigger(FunctionCallInfo fcinfo, const char *fc_funcname,
							int fc_tgkind);
static const RI_ConstraintInfo *fc_ri_FetchConstraintInfo(Trigger *fc_trigger,
													   Relation fc_trig_rel, bool fc_rel_is_pk);
static const RI_ConstraintInfo *fc_ri_LoadConstraintInfo(Oid fc_constraintOid);
static Oid	get_ri_constraint_root(Oid fc_constrOid);
static SPIPlanPtr fc_ri_PlanCheck(const char *fc_querystr, int fc_nargs, Oid *fc_argtypes,
							   RI_QueryKey *fc_qkey, Relation fc_fk_rel, Relation fc_pk_rel);
static bool fc_ri_PerformCheck(const RI_ConstraintInfo *fc_riinfo,
							RI_QueryKey *fc_qkey, SPIPlanPtr fc_qplan,
							Relation fc_fk_rel, Relation fc_pk_rel,
							TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot,
							bool fc_detectNewRows, int fc_expect_OK);
static void fc_ri_ExtractValues(Relation fc_rel, TupleTableSlot *fc_slot,
							 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk,
							 Datum *fc_vals, char *fc_nulls);
static void fc_ri_ReportViolation(const RI_ConstraintInfo *fc_riinfo,
							   Relation fc_pk_rel, Relation fc_fk_rel,
							   TupleTableSlot *fc_violatorslot, TupleDesc fc_tupdesc,
							   int fc_queryno, bool fc_partgone) pg_attribute_noreturn();


/*
 * RI_FKey_check -
 *
 * 检查外键的存在性（合并用于插入和更新）。
 */
static Datum fc_RI_FKey_check(TriggerData *fc_trigdata)
{
	const RI_ConstraintInfo *fc_riinfo;
	Relation	fc_fk_rel;
	Relation	fc_pk_rel;
	TupleTableSlot *fc_newslot;
	RI_QueryKey fc_qkey;
	SPIPlanPtr	fc_qplan;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigdata->tg_trigger,
									fc_trigdata->tg_relation, false);

	if (TRIGGER_FIRED_BY_UPDATE(fc_trigdata->tg_event))
		fc_newslot = fc_trigdata->tg_newslot;
	else
		fc_newslot = fc_trigdata->tg_trigslot;

	/*
	 * 如果行不再有效，我们甚至不应该考虑检查它，
	 * 因为它要么被删除（所以应该跳过延迟检查）
	 * 或更新（在这种情况下，只应检查行的最新版本）。
	 * 根据 SnapshotSelf 测试其存活性。我们需要对缓冲区进行锁定
	 * 和锁住以调用 HeapTupleSatisfiesVisibility。调用者
	 * 应该持有锁定，但不应持有锁。
	 */
	if (!table_tuple_satisfies_snapshot(fc_trigdata->tg_relation, fc_newslot, SnapshotSelf))
		return PointerGetDatum(NULL);

	/*
	 * 获取外键和主键表的关系描述符。
	 *
	 * pk_rel 以 RowShareLock 模式打开，因为这就是我们最终
	 * 将在其上执行的 SELECT FOR KEY SHARE。
	 */
	fc_fk_rel = fc_trigdata->tg_relation;
	fc_pk_rel = table_open(fc_riinfo->pk_relid, RowShareLock);

	switch (fc_ri_NullCheck(RelationGetDescr(fc_fk_rel), fc_newslot, fc_riinfo, false))
	{
		case RI_KEYS_ALL_NULL:

			/*
			 * 不需要进一步检查 - 全NULL键通过每种类型的
			 * 外键约束。
			 */
			table_close(fc_pk_rel, RowShareLock);
			return PointerGetDatum(NULL);

		case RI_KEYS_SOME_NULL:

			/*
			 * 这是三种 MATCH 中唯一不同的情况。
			 */
			switch (fc_riinfo->confmatchtype)
			{
				case FKCONSTR_MATCH_FULL:

					/*
					 * 不允许 - MATCH FULL 表示所有或没有属性
					 * 可以是 NULL。
					 */
					ereport(ERROR,
							(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
							 errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
									RelationGetRelationName(fc_fk_rel),
									NameStr(fc_riinfo->conname)),
							 errdetail("MATCH FULL does not allow mixing of null and nonnull key values."),
							 errtableconstraint(fc_fk_rel,
												NameStr(fc_riinfo->conname))));
					table_close(fc_pk_rel, RowShareLock);
					return PointerGetDatum(NULL);

				case FKCONSTR_MATCH_SIMPLE:

					/*
					 * MATCH SIMPLE - 如果任意列为 null，键就通过了
					 * 约束。
					 */
					table_close(fc_pk_rel, RowShareLock);
					return PointerGetDatum(NULL);

#ifdef NOT_USED
				case FKCONSTR_MATCH_PARTIAL:

					/*
					 * MATCH PARTIAL - 所有非空列必须匹配。（未实现，可以通过修改下面的查询
					 * 来仅包含非空列，或者在这里编写一个特殊版）
					 */
					break;
#endif
			}

		case RI_KEYS_NONE_NULL:

			/*
			 * 拥有完全限定的键 - 继续处理所有三种
			 * MATCH。
			 */
			break;
	}

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/* 获取或准备一个保存的计划进行实际检查 */
	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, RI_PLAN_CHECK_LOOKUPPK);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		char		fc_pkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		Oid			fc_queryoids[RI_MAX_NUMKEYS];
		const char *fc_pk_only;

		/* ----------
		 * 构建的查询字符串是
		 *	SELECT 1 FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
		 *		   FOR KEY SHARE OF x
		 * $ 参数的类型 ID 是
		 * 对应 FK 属性的。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		fc_pk_only = fc_pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_pkrelname, fc_pk_rel);
		appendStringInfo(&fc_querybuf, "SELECT 1 FROM %s%s x",
						 fc_pk_only, fc_pkrelname);
		fc_querysep = "WHERE";
		for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]));
			sprintf(fc_paramname, "$%d", fc_i + 1);
			fc_ri_GenerateQual(&fc_querybuf, fc_querysep,
							fc_attname, fc_pk_type,
							fc_riinfo->pf_eq_oprs[fc_i],
							fc_paramname, fc_fk_type);
			fc_querysep = "AND";
			fc_queryoids[fc_i] = fc_fk_type;
		}
		appendStringInfoString(&fc_querybuf, " FOR KEY SHARE OF x");

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 现在检查外键是否存在于 PK 表中
	 *
	 * XXX 当被引用侧为分区表时，detectNewRows 必须为真。
	 * 原因是我们的快照必须是最新的，以便 find_inheritance_children() 中的
	 * hack 可以工作。
	 */
	fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
					fc_fk_rel, fc_pk_rel,
					NULL, fc_newslot,
					fc_pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
					SPI_OK_SELECT);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	table_close(fc_pk_rel, RowShareLock);

	return PointerGetDatum(NULL);
}


/*
 * RI_FKey_check_ins -
 *
 * 检查在 FK 表的插入事件中外键的存在性。
 */
Datum RI_FKey_check_ins(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_check_ins", RI_TRIGTYPE_INSERT);

	/* 与 UPDATE 情况共享代码。 */
	return fc_RI_FKey_check((TriggerData *) fcinfo->context);
}


/*
 * RI_FKey_check_upd -
 *
 * 检查在 FK 表的更新事件中外键的存在性。
 */
Datum RI_FKey_check_upd(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_check_upd", RI_TRIGTYPE_UPDATE);

	/* 与 INSERT 情况共享代码。 */
	return fc_RI_FKey_check((TriggerData *) fcinfo->context);
}


/*
 * ri_Check_Pk_Match
 *
 * 检查是否创建了另一个 PK 行，该行提供与在我们触发
 * 事件中已修改或删除的 "oldslot" 相同的键值。
 * 如果在 PK 表中找到匹配项，则返回 true。
 *
 * 我们假设调用者已检查 oldslot 不包含 NULL 键值，
 * 因为否则匹配是不可能的。
 */
static bool fc_ri_Check_Pk_Match(Relation fc_pk_rel, Relation fc_fk_rel,
				  TupleTableSlot *fc_oldslot,
				  const RI_ConstraintInfo *fc_riinfo)
{
	SPIPlanPtr	fc_qplan;
	RI_QueryKey fc_qkey;
	bool		fc_result;

	/* 仅在非空行中调用 */
	Assert(fc_ri_NullCheck(RelationGetDescr(fc_pk_rel), fc_oldslot, fc_riinfo, true) == RI_KEYS_NONE_NULL);

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/*
	 * 获取或准备一个保存的计划，以检查来自 PK 行的值的 PK 表
	 */
	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, RI_PLAN_CHECK_LOOKUPPK_FROM_PK);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		char		fc_pkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		const char *fc_pk_only;
		Oid			fc_queryoids[RI_MAX_NUMKEYS];

		/* ----------
		 * 构建的查询字符串是
		 *	SELECT 1 FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
		 *		   FOR KEY SHARE OF x
		 * $ 参数的类型 ID 是
		 * PK 属性本身的类型。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		fc_pk_only = fc_pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_pkrelname, fc_pk_rel);
		appendStringInfo(&fc_querybuf, "SELECT 1 FROM %s%s x",
						 fc_pk_only, fc_pkrelname);
		fc_querysep = "WHERE";
		for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]));
			sprintf(fc_paramname, "$%d", fc_i + 1);
			fc_ri_GenerateQual(&fc_querybuf, fc_querysep,
							fc_attname, fc_pk_type,
							fc_riinfo->pp_eq_oprs[fc_i],
							fc_paramname, fc_pk_type);
			fc_querysep = "AND";
			fc_queryoids[fc_i] = fc_pk_type;
		}
		appendStringInfoString(&fc_querybuf, " FOR KEY SHARE OF x");

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 我们现在有一个计划。执行它。
	 */
	fc_result = fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
							 fc_fk_rel, fc_pk_rel,
							 fc_oldslot, NULL,
							 true,	/* 处理如同更新 */
							 SPI_OK_SELECT);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	return fc_result;
}


/*
 * RI_FKey_noaction_del -
 *
 * 如果删除导致违反给定的引用完整性约束，则产生错误并回滚当前事务。
 */
Datum RI_FKey_noaction_del(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_noaction_del", RI_TRIGTYPE_DELETE);

	/* 与 RESTRICT/UPDATE 情况共享代码。 */
	return fc_ri_restrict((TriggerData *) fcinfo->context, true);
}

/*
 * RI_FKey_restrict_del -
 *
 * 限制从 PK 表删除未被外键引用的行。
 *
 * SQL 标准意图是该引用操作在执行删除时准确发生，而不是之后。这似乎是“NO ACTION”和“RESTRICT”之间唯一的区别。在 Postgres 中，我们仍然将其实现为 AFTER 触发器，但它是不可延迟的。
 */
Datum RI_FKey_restrict_del(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_restrict_del", RI_TRIGTYPE_DELETE);

	/* 与 NO ACTION/UPDATE 情况共享代码。 */
	return fc_ri_restrict((TriggerData *) fcinfo->context, false);
}

/*
 * RI_FKey_noaction_upd -
 *
 * 如果更新导致违反给定的引用完整性约束，则产生错误并回滚当前事务。
 */
Datum RI_FKey_noaction_upd(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE);

	/* 与 RESTRICT/DELETE 情况共享代码。 */
	return fc_ri_restrict((TriggerData *) fcinfo->context, true);
}

/*
 * RI_FKey_restrict_upd -
 *
 * 限制对 PK 的更新，使其不被外键引用的行。
 *
 * SQL 标准意图是该引用操作在执行更新时准确发生，而不是之后。这似乎是“NO ACTION”和“RESTRICT”之间唯一的区别。在 Postgres 中，我们仍然将其实现为 AFTER 触发器，但它是不可延迟的。
 */
Datum RI_FKey_restrict_upd(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE);

	/* 与 NO ACTION/DELETE 情况共享代码。 */
	return fc_ri_restrict((TriggerData *) fcinfo->context, false);
}

/*
 * ri_restrict -
 *
 * ON DELETE RESTRICT、ON DELETE NO ACTION、ON UPDATE RESTRICT 和 ON UPDATE NO ACTION 的通用代码。
 */
static Datum fc_ri_restrict(TriggerData *fc_trigdata, bool fc_is_no_action)
{
	const RI_ConstraintInfo *fc_riinfo;
	Relation	fc_fk_rel;
	Relation	fc_pk_rel;
	TupleTableSlot *fc_oldslot;
	RI_QueryKey fc_qkey;
	SPIPlanPtr	fc_qplan;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigdata->tg_trigger,
									fc_trigdata->tg_relation, true);

	/*
	 * 获取 FK 和 PK 表的关系描述符以及旧元组。
	 *
	 * fk_rel 在 RowShareLock 模式下打开，因为我们的最终
	 * SELECT FOR KEY SHARE 将在其上进行。
	 */
	fc_fk_rel = table_open(fc_riinfo->fk_relid, RowShareLock);
	fc_pk_rel = fc_trigdata->tg_relation;
	fc_oldslot = fc_trigdata->tg_trigslot;

	/*
	 * 如果现在存在提供旧键值的另一个 PK 行，我们就不应执行任何操作。
	 * 然而，此检查仅应在 NO ACTION 情况下进行；在 RESTRICT 情况下，我们不希望允许另一个行被替代。
	 */
	if (fc_is_no_action &&
		fc_ri_Check_Pk_Match(fc_pk_rel, fc_fk_rel, fc_oldslot, fc_riinfo))
	{
		table_close(fc_fk_rel, RowShareLock);
		return PointerGetDatum(NULL);
	}

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/*
	 * 获取或准备一个用于限制查找的保存计划（对于删除和更新情况来说查询是相同的）
	 */
	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, RI_PLAN_RESTRICT);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		Oid			fc_queryoids[RI_MAX_NUMKEYS];
		const char *fc_fk_only;

		/* ----------
		 * 构建的查询字符串是
		 *	SELECT 1 FROM [ONLY] <fktable> x WHERE $1 = fkatt1 [AND ...]
		 *		   FOR KEY SHARE OF x
		 * $ 参数的类型 ID 是相应 PK 属性的类型 ID。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
		appendStringInfo(&fc_querybuf, "SELECT 1 FROM %s%s x",
						 fc_fk_only, fc_fkrelname);
		fc_querysep = "WHERE";
		for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
			Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
			sprintf(fc_paramname, "$%d", fc_i + 1);
			fc_ri_GenerateQual(&fc_querybuf, fc_querysep,
							fc_paramname, fc_pk_type,
							fc_riinfo->pf_eq_oprs[fc_i],
							fc_attname, fc_fk_type);
			if (fc_pk_coll != fc_fk_coll && !get_collation_isdeterministic(fc_pk_coll))
				fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
			fc_querysep = "AND";
			fc_queryoids[fc_i] = fc_pk_type;
		}
		appendStringInfoString(&fc_querybuf, " FOR KEY SHARE OF x");

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 我们现在有一个计划。运行它以检查现有引用。
	 */
	fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
					fc_fk_rel, fc_pk_rel,
					fc_oldslot, NULL,
					true,		/* 必须检测新行 */
					SPI_OK_SELECT);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	table_close(fc_fk_rel, RowShareLock);

	return PointerGetDatum(NULL);
}


/*
 * RI_FKey_cascade_del -
 *
 * 在 PK 表的删除事件时级联删除外键引用。
 */
Datum RI_FKey_cascade_del(PG_FUNCTION_ARGS)
{
	TriggerData *fc_trigdata = (TriggerData *) fcinfo->context;
	const RI_ConstraintInfo *fc_riinfo;
	Relation	fc_fk_rel;
	Relation	fc_pk_rel;
	TupleTableSlot *fc_oldslot;
	RI_QueryKey fc_qkey;
	SPIPlanPtr	fc_qplan;

	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_cascade_del", RI_TRIGTYPE_DELETE);

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigdata->tg_trigger,
									fc_trigdata->tg_relation, true);

	/*
	 * 获取 FK 和 PK 表的关系描述符以及旧元组。
	 *
	 * fk_rel 在 RowExclusiveLock 模式下打开，因为我们的最终 DELETE 将在其上进行。
	 */
	fc_fk_rel = table_open(fc_riinfo->fk_relid, RowExclusiveLock);
	fc_pk_rel = fc_trigdata->tg_relation;
	fc_oldslot = fc_trigdata->tg_trigslot;

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/* 获取或准备一个用于级联删除的保存计划 */
	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, RI_PLAN_CASCADE_ONDELETE);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		Oid			fc_queryoids[RI_MAX_NUMKEYS];
		const char *fc_fk_only;

		/* ----------
		 * 构建的查询字符串是
		 *	DELETE FROM [ONLY] <fktable> WHERE $1 = fkatt1 [AND ...]
		 * $ 参数的类型 ID 是相应 PK 属性的类型 ID。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
		appendStringInfo(&fc_querybuf, "DELETE FROM %s%s",
						 fc_fk_only, fc_fkrelname);
		fc_querysep = "WHERE";
		for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
			Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
			sprintf(fc_paramname, "$%d", fc_i + 1);
			fc_ri_GenerateQual(&fc_querybuf, fc_querysep,
							fc_paramname, fc_pk_type,
							fc_riinfo->pf_eq_oprs[fc_i],
							fc_attname, fc_fk_type);
			if (fc_pk_coll != fc_fk_coll && !get_collation_isdeterministic(fc_pk_coll))
				fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
			fc_querysep = "AND";
			fc_queryoids[fc_i] = fc_pk_type;
		}

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 我们现在有一个计划。从已删除的 PK 元组中的键值建立参数，并删除引用行。
	 */
	fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
					fc_fk_rel, fc_pk_rel,
					fc_oldslot, NULL,
					true,		/* 必须检测新行 */
					SPI_OK_DELETE);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	table_close(fc_fk_rel, RowExclusiveLock);

	return PointerGetDatum(NULL);
}


/*
 * RI_FKey_cascade_upd -
 *
 * 在 PK 表的更新事件时级联更新外键引用。
 */
Datum RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
{
	TriggerData *fc_trigdata = (TriggerData *) fcinfo->context;
	const RI_ConstraintInfo *fc_riinfo;
	Relation	fc_fk_rel;
	Relation	fc_pk_rel;
	TupleTableSlot *fc_newslot;
	TupleTableSlot *fc_oldslot;
	RI_QueryKey fc_qkey;
	SPIPlanPtr	fc_qplan;

	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_cascade_upd", RI_TRIGTYPE_UPDATE);

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigdata->tg_trigger,
									fc_trigdata->tg_relation, true);

	/*
	 * 获取 FK 和 PK 表的关系描述符，以及新的和旧的元组。
	 *
	 * fk_rel 在 RowExclusiveLock 模式下打开，因为我们的最终 UPDATE 将在其上进行。
	 */
	fc_fk_rel = table_open(fc_riinfo->fk_relid, RowExclusiveLock);
	fc_pk_rel = fc_trigdata->tg_relation;
	fc_newslot = fc_trigdata->tg_newslot;
	fc_oldslot = fc_trigdata->tg_trigslot;

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/* 获取或准备一个用于级联更新的保存计划 */
	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, RI_PLAN_CASCADE_ONUPDATE);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		StringInfoData fc_qualbuf;
		char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		const char *fc_qualsep;
		Oid			fc_queryoids[RI_MAX_NUMKEYS * 2];
		const char *fc_fk_only;

		/* ----------
		 * 构建的查询字符串是
		 *	UPDATE [ONLY] <fktable> SET fkatt1 = $1 [, ...]
		 *			WHERE $n = fkatt1 [AND ...]
		 * $ 参数的类型 ID 是相应 PK 属性的类型 ID。 注意，我们假设
		 * 从 PK 到 FK 类型存在一个赋值转换；
		 * 否则解析器将失败。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		initStringInfo(&fc_qualbuf);
		fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
		appendStringInfo(&fc_querybuf, "UPDATE %s%s SET",
						 fc_fk_only, fc_fkrelname);
		fc_querysep = "";
		fc_qualsep = "WHERE";
		for (int fc_i = 0, fc_j = fc_riinfo->nkeys; fc_i < fc_riinfo->nkeys; fc_i++, fc_j++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
			Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
			appendStringInfo(&fc_querybuf,
							 "%s %s = $%d",
							 fc_querysep, fc_attname, fc_i + 1);
			sprintf(fc_paramname, "$%d", fc_j + 1);
			fc_ri_GenerateQual(&fc_qualbuf, fc_qualsep,
							fc_paramname, fc_pk_type,
							fc_riinfo->pf_eq_oprs[fc_i],
							fc_attname, fc_fk_type);
			if (fc_pk_coll != fc_fk_coll && !get_collation_isdeterministic(fc_pk_coll))
				fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
			fc_querysep = ",";
			fc_qualsep = "AND";
			fc_queryoids[fc_i] = fc_pk_type;
			fc_queryoids[fc_j] = fc_pk_type;
		}
		appendBinaryStringInfo(&fc_querybuf, fc_qualbuf.data, fc_qualbuf.len);

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys * 2, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 我们现在有一个计划。运行它以更新现有引用。
	 */
	fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
					fc_fk_rel, fc_pk_rel,
					fc_oldslot, fc_newslot,
					true,		/* 必须检测新行 */
					SPI_OK_UPDATE);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	table_close(fc_fk_rel, RowExclusiveLock);

	return PointerGetDatum(NULL);
}


/*
 * RI_FKey_setnull_del -
 *
 * 在 PK 表的删除事件时将外键引用设置为 NULL 值。
 */
Datum RI_FKey_setnull_del(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_setnull_del", RI_TRIGTYPE_DELETE);

	/* 与 UPDATE 情况共享代码 */
	return fc_ri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_DELETE);
}

/*
 * RI_FKey_setnull_upd -
 *
 * 在 PK 表的更新事件时将外键引用设置为 NULL。
 */
Datum RI_FKey_setnull_upd(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_setnull_upd", RI_TRIGTYPE_UPDATE);

	/* 与 DELETE 情况共享代码 */
	return fc_ri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_UPDATE);
}

/*
 * RI_FKey_setdefault_del -
 *
 * 在 PK 表的删除事件时将外键引用设置为默认值。
 */
Datum RI_FKey_setdefault_del(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_setdefault_del", RI_TRIGTYPE_DELETE);

	/* 与 UPDATE 情况共享代码 */
	return fc_ri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_DELETE);
}

/*
 * RI_FKey_setdefault_upd -
 *
 * 在主键表的更新事件中将外键引用设置为默认值。
 */
Datum RI_FKey_setdefault_upd(PG_FUNCTION_ARGS)
{
	/* 检查在正确的时间和事件上是否这是一个有效的触发器调用。 */
	fc_ri_CheckTrigger(fcinfo, "RI_FKey_setdefault_upd", RI_TRIGTYPE_UPDATE);

	/* 与 DELETE 情况共享代码 */
	return fc_ri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_UPDATE);
}

/*
 * ri_set -
 *
 * 处理 ON DELETE SET NULL、ON DELETE SET DEFAULT、ON UPDATE SET
 * NULL 和 ON UPDATE SET DEFAULT 的通用代码。
 */
static Datum fc_ri_set(TriggerData *fc_trigdata, bool fc_is_set_null, int fc_tgkind)
{
	const RI_ConstraintInfo *fc_riinfo;
	Relation	fc_fk_rel;
	Relation	fc_pk_rel;
	TupleTableSlot *fc_oldslot;
	RI_QueryKey fc_qkey;
	SPIPlanPtr	fc_qplan;
	int32		fc_queryno;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigdata->tg_trigger,
									fc_trigdata->tg_relation, true);

	/*
	 * 获取外键和主键表以及旧元组的关系描述符。
	 *
	 * fk_rel 以 RowExclusiveLock 模式打开，因为这正是我们
	 * 最终 UPDATE 将对其使用的模式。
	 */
	fc_fk_rel = table_open(fc_riinfo->fk_relid, RowExclusiveLock);
	fc_pk_rel = fc_trigdata->tg_relation;
	fc_oldslot = fc_trigdata->tg_trigslot;

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/*
	 * 获取或准备触发器的保存计划。
	 */
	switch (fc_tgkind)
	{
		case RI_TRIGTYPE_UPDATE:
			fc_queryno = fc_is_set_null
				? RI_PLAN_SETNULL_ONUPDATE
				: RI_PLAN_SETDEFAULT_ONUPDATE;
			break;
		case RI_TRIGTYPE_DELETE:
			fc_queryno = fc_is_set_null
				? RI_PLAN_SETNULL_ONDELETE
				: RI_PLAN_SETDEFAULT_ONDELETE;
			break;
		default:
			elog(ERROR, "invalid tgkind passed to ri_set");
	}

	fc_ri_BuildQueryKey(&fc_qkey, fc_riinfo, fc_queryno);

	if ((fc_qplan = fc_ri_FetchPreparedPlan(&fc_qkey)) == NULL)
	{
		StringInfoData fc_querybuf;
		char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
		char		fc_attname[MAX_QUOTED_NAME_LEN];
		char		fc_paramname[16];
		const char *fc_querysep;
		const char *fc_qualsep;
		Oid			fc_queryoids[RI_MAX_NUMKEYS];
		const char *fc_fk_only;
		int			fc_num_cols_to_set;
		const int16 *set_cols;

		switch (fc_tgkind)
		{
			case RI_TRIGTYPE_UPDATE:
				fc_num_cols_to_set = fc_riinfo->nkeys;
				set_cols = fc_riinfo->fk_attnums;
				break;
			case RI_TRIGTYPE_DELETE:

				/*
				 * 如果 confdelsetcols 存在，则我们只更新
				 * 在该数组中指定的列，否则我们更新所有
				 * 引用列。
				 */
				if (fc_riinfo->ndelsetcols != 0)
				{
					fc_num_cols_to_set = fc_riinfo->ndelsetcols;
					set_cols = fc_riinfo->confdelsetcols;
				}
				else
				{
					fc_num_cols_to_set = fc_riinfo->nkeys;
					set_cols = fc_riinfo->fk_attnums;
				}
				break;
			default:
				elog(ERROR, "invalid tgkind passed to ri_set");
		}

		/* ----------
		 * 构建的查询字符串是
		 *	UPDATE [ONLY] <fktable> SET fkatt1 = {NULL|DEFAULT} [, ...]
		 *			WHERE $1 = fkatt1 [AND ...]
		 * $ 参数的类型 ID 是对应 PK 属性的类型 ID。
		 * ----------
		 */
		initStringInfo(&fc_querybuf);
		fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
			"" : "ONLY ";
		fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
		appendStringInfo(&fc_querybuf, "UPDATE %s%s SET",
						 fc_fk_only, fc_fkrelname);

		/*
		 * 添加赋值子句
		 */
		fc_querysep = "";
		for (int fc_i = 0; fc_i < fc_num_cols_to_set; fc_i++)
		{
			fc_quoteOneName(fc_attname, RIAttName(fc_fk_rel, set_cols[fc_i]));
			appendStringInfo(&fc_querybuf,
							 "%s %s = %s",
							 fc_querysep, fc_attname,
							 fc_is_set_null ? "NULL" : "DEFAULT");
			fc_querysep = ",";
		}

		/*
		 * 添加 WHERE 子句
		 */
		fc_qualsep = "WHERE";
		for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
		{
			Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
			Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
			Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

			fc_quoteOneName(fc_attname,
						 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));

			sprintf(fc_paramname, "$%d", fc_i + 1);
			fc_ri_GenerateQual(&fc_querybuf, fc_qualsep,
							fc_paramname, fc_pk_type,
							fc_riinfo->pf_eq_oprs[fc_i],
							fc_attname, fc_fk_type);
			if (fc_pk_coll != fc_fk_coll && !get_collation_isdeterministic(fc_pk_coll))
				fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
			fc_qualsep = "AND";
			fc_queryoids[fc_i] = fc_pk_type;
		}

		/* 准备并保存计划 */
		fc_qplan = fc_ri_PlanCheck(fc_querybuf.data, fc_riinfo->nkeys, fc_queryoids,
							 &fc_qkey, fc_fk_rel, fc_pk_rel);
	}

	/*
	 * 我们现在有一个计划。运行它以更新现有引用。
	 */
	fc_ri_PerformCheck(fc_riinfo, &fc_qkey, fc_qplan,
					fc_fk_rel, fc_pk_rel,
					fc_oldslot, NULL,
					true,		/* 必须检测新行 */
					SPI_OK_UPDATE);

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	table_close(fc_fk_rel, RowExclusiveLock);

	if (fc_is_set_null)
		return PointerGetDatum(NULL);
	else
	{
		/*
		 * 如果我们刚刚删除或更新了主键行，其键等于外键列的默认值，并且在外键
		 * 表中存在引用行，则我们会将该行更新为它已经拥有的相同值——
		 * 而 RI_FKey_fk_upd_check_required 因此会认为没有必要检查。
		 * 所以我们需要现在再执行一次查找，以防仍然存在引用， abort 操作。
		 * 这在 NO ACTION 触发器中已实现，因此只需运行它。(此
		 * 重新检查仅在 SET DEFAULT 情况下需要，因为 CASCADE 在删除操作的情况下会
		 * 移除此类行或在更新情况下会更改外键键值，而 SET NULL 确定会
		 * 导致满足外键约束的行。)
		 */
		return fc_ri_restrict(fc_trigdata, true);
	}
}


/*
 * RI_FKey_pk_upd_check_required -
 *
 * 检查我们是否真的需要触发 RI 触发器以更新或删除主键
 * 关系。这是由 AFTER 触发器队列管理器调用，以查看
 * 是否可以跳过排队 RI 触发器的实例。如果触发器必须被触发，则返回 true，
 * 如果我们可以证明约束仍将得到满足，则返回 false。
 *
 * 如果这是对删除的调用，则 newslot 将是 NULL。
 */
bool RI_FKey_pk_upd_check_required(Trigger *fc_trigger, Relation fc_pk_rel,
							  TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot)
{
	const RI_ConstraintInfo *fc_riinfo;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigger, fc_pk_rel, true);

	/*
	 * 如果任何旧键值为 NULL，则该行不能被外键行引用，
	 * 所以不需要检查。
	 */
	if (fc_ri_NullCheck(RelationGetDescr(fc_pk_rel), fc_oldslot, fc_riinfo, true) != RI_KEYS_NONE_NULL)
		return false;

	/* 如果所有旧键值和新键值相等，则不需要检查 */
	if (fc_newslot && fc_ri_KeysEqual(fc_pk_rel, fc_oldslot, fc_newslot, fc_riinfo, true))
		return false;

	/* 否则我们需要触发触发器。 */
	return true;
}

/*
 * RI_FKey_fk_upd_check_required -
 *
 * 检查我们是否真的需要触发 RI 触发器以更新外键
 * 关系。这是由 AFTER 触发器队列管理器调用，以查看
 * 是否可以跳过排队 RI 触发器的实例。如果触发器必须被触发，则返回 true，
 * 如果我们可以证明约束仍将得到满足，则返回 false。
 */
bool RI_FKey_fk_upd_check_required(Trigger *fc_trigger, Relation fc_fk_rel,
							  TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot)
{
	const RI_ConstraintInfo *fc_riinfo;
	int			fc_ri_nullcheck;
	Datum		fc_xminDatum;
	TransactionId fc_xmin;
	bool		fc_isnull;

	/*
	 * AfterTriggerSaveEvent() 处理事情，以便此函数在分区表上
	 * 从未被调用。
	 */
	Assert(fc_fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigger, fc_fk_rel, false);

	fc_ri_nullcheck = fc_ri_NullCheck(RelationGetDescr(fc_fk_rel), fc_newslot, fc_riinfo, false);

	/*
	 * 如果所有的新键值为 NULL，则该行满足约束，因此不需要
	 * 检查。
	 */
	if (fc_ri_nullcheck == RI_KEYS_ALL_NULL)
		return false;

	/*
	 * 如果某些新键值为 NULL，则行为依赖于匹配类型。
	 */
	else if (fc_ri_nullcheck == RI_KEYS_SOME_NULL)
	{
		switch (fc_riinfo->confmatchtype)
		{
			case FKCONSTR_MATCH_SIMPLE:

				/*
				 * 如果任何新键值为 NULL，则该行必须满足
				 * 约束，因此不需要检查。
				 */
				return false;

			case FKCONSTR_MATCH_PARTIAL:

				/*
				 * 不知道，必须进行完整检查。
				 */
				break;

			case FKCONSTR_MATCH_FULL:

				/*
				 * 如果某些新键值为 NULL，则该行未能满足
				 * 约束。我们在这里不能抛出错误，因为该行
				 * 可能在约束检查之前就失效，但我们应该队列事件以稍后应用检查。
				 */
				return true;
		}
	}

	/*
	 * 继续这里，因为没有新键值为 NULL，或者我们尚未决定。
	 */

	/*
	 * 如果原始行是由我们自己的事务插入的，无论键是否相等，我们都必须触发
	 * 触发器。这是因为我们的
	 * UPDATE 将使 INSERT 失效，以便 INSERT RI 触发器
	 * 不会做任何事情；因此我们最好执行 UPDATE 检查。
	 * (如果我们知道 INSERT 触发器已经被触发，我们可以跳过这一步，
	 * 但没有简单的方法来知道这一点。)
	 */
	fc_xminDatum = slot_getsysattr(fc_oldslot, MinTransactionIdAttributeNumber, &fc_isnull);
	Assert(!fc_isnull);
	fc_xmin = DatumGetTransactionId(fc_xminDatum);
	if (TransactionIdIsCurrentTransactionId(fc_xmin))
		return true;

	/* 如果所有旧键值和新键值相等，则不需要检查 */
	if (fc_ri_KeysEqual(fc_fk_rel, fc_oldslot, fc_newslot, fc_riinfo, false))
		return false;

	/* 否则我们需要触发触发器。 */
	return true;
}


/*
 * RI_Initial_Check -
 *
 * 检查整个表的非匹配值，使用单个查询。
 * 这不是触发器过程，但在 ALTER TABLE 中被调用
 * 添加外键以验证初始表内容。
 *
 * 我们期望调用者已采取措施防止任何由并发操作引起的问题。
 * 这可能是通过以 ShareRowExclusiveLock 或更高级别锁定 rel 和
 * pkrel，或通过以其他方式确保
 * 实现检查的触发器已经激活。
 * 因此，我们不需要为检查锁定单独的行。
 *
 * 如果检查失败，因为当前用户没有权限
 * 读取两个表，返回 false 以向我们的调用者表明他们将
 * 需要采取其他措施来检查约束。
 */
bool RI_Initial_Check(Trigger *fc_trigger, Relation fc_fk_rel, Relation fc_pk_rel)
{
	const RI_ConstraintInfo *fc_riinfo;
	StringInfoData fc_querybuf;
	char		fc_pkrelname[MAX_QUOTED_REL_NAME_LEN];
	char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
	char		fc_pkattname[MAX_QUOTED_NAME_LEN + 3];
	char		fc_fkattname[MAX_QUOTED_NAME_LEN + 3];
	RangeTblEntry *fc_pkrte;
	RangeTblEntry *fc_fkrte;
	const char *fc_sep;
	const char *fc_fk_only;
	const char *fc_pk_only;
	int			fc_save_nestlevel;
	char		fc_workmembuf[32];
	int			fc_spi_result;
	SPIPlanPtr	fc_qplan;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigger, fc_fk_rel, false);

	/*
	 * 检查确保当前用户具有足够的权限进行测试
	 * 查询。 （如果没有，调用者可以退回到触发器方法，这
	 * 是有效的，因为它会实时更改用户 ID。）
	 *
	 * XXX 有没有其他会导致故障的条件需要检查？
	 */
	fc_pkrte = makeNode(RangeTblEntry);
	fc_pkrte->rtekind = RTE_RELATION;
	fc_pkrte->relid = RelationGetRelid(fc_pk_rel);
	fc_pkrte->relkind = fc_pk_rel->rd_rel->relkind;
	fc_pkrte->rellockmode = AccessShareLock;
	fc_pkrte->requiredPerms = ACL_SELECT;

	fc_fkrte = makeNode(RangeTblEntry);
	fc_fkrte->rtekind = RTE_RELATION;
	fc_fkrte->relid = RelationGetRelid(fc_fk_rel);
	fc_fkrte->relkind = fc_fk_rel->rd_rel->relkind;
	fc_fkrte->rellockmode = AccessShareLock;
	fc_fkrte->requiredPerms = ACL_SELECT;

	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		int			fc_attno;

		fc_attno = fc_riinfo->pk_attnums[fc_i] - FirstLowInvalidHeapAttributeNumber;
		fc_pkrte->selectedCols = bms_add_member(fc_pkrte->selectedCols, fc_attno);

		fc_attno = fc_riinfo->fk_attnums[fc_i] - FirstLowInvalidHeapAttributeNumber;
		fc_fkrte->selectedCols = bms_add_member(fc_fkrte->selectedCols, fc_attno);
	}

	if (!ExecCheckRTPerms(list_make2(fc_fkrte, fc_pkrte), false))
		return false;

	/*
	 * 如果任一表启用了 RLS，除非该角色具有
	 * bypassrls 权限或是涉及启用 RLS 的表的表所有者，
	 * 否则也可以跳过。
	 */
	if (!has_bypassrls_privilege(GetUserId()) &&
		((fc_pk_rel->rd_rel->relrowsecurity &&
		  !pg_class_ownercheck(fc_pkrte->relid, GetUserId())) ||
		 (fc_fk_rel->rd_rel->relrowsecurity &&
		  !pg_class_ownercheck(fc_fkrte->relid, GetUserId()))))
		return false;

	/*----------
	 * 构建的查询字符串是：
	 *	SELECT fk.keycols FROM [ONLY] relname fk
	 *	 LEFT OUTER JOIN [ONLY] pkrelname pk
	 *	 ON (pk.pkkeycol1=fk.keycol1 [AND ...])
	 *	 WHERE pk.pkkeycol1 IS NULL AND
	 * 对于 MATCH SIMPLE：
	 *	 (fk.keycol1 IS NOT NULL [AND ...])
	 * 对于 MATCH FULL：
	 *	 (fk.keycol1 IS NOT NULL [OR ...])
	 *
	 * 在比较具有不同排序的列时，我们附加 COLLATE 子句
	 * 到操作符上。
	 *----------
	 */
	initStringInfo(&fc_querybuf);
	appendStringInfoString(&fc_querybuf, "SELECT ");
	fc_sep = "";
	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		fc_quoteOneName(fc_fkattname,
					 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		appendStringInfo(&fc_querybuf, "%sfk.%s", fc_sep, fc_fkattname);
		fc_sep = ", ";
	}

	fc_quoteRelationName(fc_pkrelname, fc_pk_rel);
	fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
	fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
		"" : "ONLY ";
	fc_pk_only = fc_pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
		"" : "ONLY ";
	appendStringInfo(&fc_querybuf,
					 " FROM %s%s fk LEFT OUTER JOIN %s%s pk ON",
					 fc_fk_only, fc_fkrelname, fc_pk_only, fc_pkrelname);

	strcpy(fc_pkattname, "pk.");
	strcpy(fc_fkattname, "fk.");
	fc_sep = "(";
	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
		Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
		Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
		Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

		fc_quoteOneName(fc_pkattname + 3,
					 RIAttName(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]));
		fc_quoteOneName(fc_fkattname + 3,
					 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		fc_ri_GenerateQual(&fc_querybuf, fc_sep,
						fc_pkattname, fc_pk_type,
						fc_riinfo->pf_eq_oprs[fc_i],
						fc_fkattname, fc_fk_type);
		if (fc_pk_coll != fc_fk_coll)
			fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
		fc_sep = "AND";
	}

	/*
	 * 仅测试任一 pk 属性是否为 null 就足以检测连接
	 * 失败。
	 */
	fc_quoteOneName(fc_pkattname, RIAttName(fc_pk_rel, fc_riinfo->pk_attnums[0]));
	appendStringInfo(&fc_querybuf, ") WHERE pk.%s IS NULL AND (", fc_pkattname);

	fc_sep = "";
	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		fc_quoteOneName(fc_fkattname, RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		appendStringInfo(&fc_querybuf,
						 "%sfk.%s IS NOT NULL",
						 fc_sep, fc_fkattname);
		switch (fc_riinfo->confmatchtype)
		{
			case FKCONSTR_MATCH_SIMPLE:
				fc_sep = " AND ";
				break;
			case FKCONSTR_MATCH_FULL:
				fc_sep = " OR ";
				break;
		}
	}
	appendStringInfoChar(&fc_querybuf, ')');

	/*
	 * 临时增加 work_mem，以便更有效地执行检查查询。
	 * 这样做似乎没有问题，因为查询足够简单，不会使用
	 * 多倍的 work_mem，而且通常不会有许多大的外键验证
	 * 同时进行。因此这似乎符合被视为“维护”
	 * 操作的标准，因此我们使用 maintenance_work_mem。 
	 * 但是，我们还必须将 hash_mem_multiplier 设置为 1，因为
	 * 让它应用于 maintenance_work_mem 值显然是不允许的。
	 *
	 * 我们使用等效于函数 SET 选项的方法，以允许设置在
	 * 检查查询的确切持续时间内保持。guc.c 也负责在错误
	 * 时撤销设置。
	 */
	fc_save_nestlevel = NewGUCNestLevel();

	snprintf(fc_workmembuf, sizeof(fc_workmembuf), "%d", maintenance_work_mem);
	(void) set_config_option("work_mem", fc_workmembuf,
							 PGC_USERSET, PGC_S_SESSION,
							 GUC_ACTION_SAVE, true, 0, false);
	(void) set_config_option("hash_mem_multiplier", "1",
							 PGC_USERSET, PGC_S_SESSION,
							 GUC_ACTION_SAVE, true, 0, false);

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/*
	 * 生成计划。我们不需要缓存它，并且没有
	 * 传递给计划的参数。
	 */
	fc_qplan = SPI_prepare(fc_querybuf.data, 0, NULL);

	if (fc_qplan == NULL)
		elog(ERROR, "SPI_prepare returned %s for %s",
			 SPI_result_code_string(SPI_result), fc_querybuf.data);

	/*
	 * 执行计划。出于安全考虑，我们强制使用当前快照。
	 * （在事务快照模式下，这可以说违反了事务隔离
	 * 规则，但我们实际上没有太多选择。）我们不需要
	 * 注册快照，因为 SPI_execute_snapshot 会处理这个问题。
	 * 我们最多需要返回一个元组，因此传递 limit = 1。
	 */
	fc_spi_result = SPI_execute_snapshot(fc_qplan,
									  NULL, NULL,
									  GetLatestSnapshot(),
									  InvalidSnapshot,
									  true, false, 1);

	/* 检查结果 */
	if (fc_spi_result != SPI_OK_SELECT)
		elog(ERROR, "SPI_execute_snapshot returned %s", SPI_result_code_string(fc_spi_result));

	/* 我们是否找到了违反约束的元组？ */
	if (SPI_processed > 0)
	{
		TupleTableSlot *fc_slot;
		HeapTuple	fc_tuple = SPI_tuptable->vals[0];
		TupleDesc	fc_tupdesc = SPI_tuptable->tupdesc;
		RI_ConstraintInfo fc_fake_riinfo;

		fc_slot = MakeSingleTupleTableSlot(fc_tupdesc, &TTSOpsVirtual);

		heap_deform_tuple(fc_tuple, fc_tupdesc,
						  fc_slot->tts_values, fc_slot->tts_isnull);
		ExecStoreVirtualTuple(fc_slot);

		/*
		 * 在结果元组中查看的列是 1..N，而不是在 fk_rel 中的列。 修改 riinfo，以便这里调用的子例程能够正常工作。
		 *
		 * 除此之外，我们必须将正确的 tupdesc 传递给 ri_ReportViolation，覆盖其使用 pk_rel 或 fk_rel 的 tupdesc 的正常习惯。
		 */
		memcpy(&fc_fake_riinfo, fc_riinfo, sizeof(RI_ConstraintInfo));
		for (int fc_i = 0; fc_i < fc_fake_riinfo.nkeys; fc_i++)
			fc_fake_riinfo.fk_attnums[fc_i] = fc_i + 1;

		/*
		 * 如果是 MATCH FULL，并且 FK 键中有任何 null，抱怨这一点，而不是缺少匹配。 MATCH FULL 不允许部分 null 的 FK 行。
		 */
		if (fc_fake_riinfo.confmatchtype == FKCONSTR_MATCH_FULL &&
			fc_ri_NullCheck(fc_tupdesc, fc_slot, &fc_fake_riinfo, false) != RI_KEYS_NONE_NULL)
			ereport(ERROR,
					(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
					 errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
							RelationGetRelationName(fc_fk_rel),
							NameStr(fc_fake_riinfo.conname)),
					 errdetail("MATCH FULL does not allow mixing of null and nonnull key values."),
					 errtableconstraint(fc_fk_rel,
										NameStr(fc_fake_riinfo.conname))));

		/*
		 * 我们告诉 ri_ReportViolation 我们正在执行 RI_PLAN_CHECK_LOOKUPPK 查询，这不是事实，但会导致它使用 fake_riinfo.fk_attnums，正如我们需要的那样。
		 */
		fc_ri_ReportViolation(&fc_fake_riinfo,
						   fc_pk_rel, fc_fk_rel,
						   fc_slot, fc_tupdesc,
						   RI_PLAN_CHECK_LOOKUPPK, false);

		ExecDropSingleTupleTableSlot(fc_slot);
	}

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	/*
	 * 恢复 work_mem 和 hash_mem_multiplier。
	 */
	AtEOXact_GUC(true, fc_save_nestlevel);

	return true;
}

/*
 * RI_PartitionRemove_Check -
 *
 * 在外键约束的引用侧分离分区时，验证是否不存在引用值。
 */
void RI_PartitionRemove_Check(Trigger *fc_trigger, Relation fc_fk_rel, Relation fc_pk_rel)
{
	const RI_ConstraintInfo *fc_riinfo;
	StringInfoData fc_querybuf;
	char	   *fc_constraintDef;
	char		fc_pkrelname[MAX_QUOTED_REL_NAME_LEN];
	char		fc_fkrelname[MAX_QUOTED_REL_NAME_LEN];
	char		fc_pkattname[MAX_QUOTED_NAME_LEN + 3];
	char		fc_fkattname[MAX_QUOTED_NAME_LEN + 3];
	const char *fc_sep;
	const char *fc_fk_only;
	int			fc_save_nestlevel;
	char		fc_workmembuf[32];
	int			fc_spi_result;
	SPIPlanPtr	fc_qplan;
	int			fc_i;

	fc_riinfo = fc_ri_FetchConstraintInfo(fc_trigger, fc_fk_rel, false);

	/*
	 * 在显示错误消息之前，我们不检查权限，假设分离分区的用户必须拥有足够的权限来查看表内容。
	 */

	/*----------
	 * 构建的查询字符串是：
	 *  SELECT fk.keycols FROM [ONLY] relname fk
	 *    JOIN pkrelname pk
	 *    ON (pk.pkkeycol1=fk.keycol1 [AND ...])
	 *    WHERE (<partition constraint>) AND
	 * 对于 MATCH SIMPLE：
	 *   (fk.keycol1 IS NOT NULL [AND ...])
	 * 对于 MATCH FULL：
	 *   (fk.keycol1 IS NOT NULL [OR ...])
	 *
	 * 在比较具有不同排序规则的列时，我们会将 COLLATE 子句附加到运算符。
	 *----------
	 */
	initStringInfo(&fc_querybuf);
	appendStringInfoString(&fc_querybuf, "SELECT ");
	fc_sep = "";
	for (fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		fc_quoteOneName(fc_fkattname,
					 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		appendStringInfo(&fc_querybuf, "%sfk.%s", fc_sep, fc_fkattname);
		fc_sep = ", ";
	}

	fc_quoteRelationName(fc_pkrelname, fc_pk_rel);
	fc_quoteRelationName(fc_fkrelname, fc_fk_rel);
	fc_fk_only = fc_fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
		"" : "ONLY ";
	appendStringInfo(&fc_querybuf,
					 " FROM %s%s fk JOIN %s pk ON",
					 fc_fk_only, fc_fkrelname, fc_pkrelname);
	strcpy(fc_pkattname, "pk.");
	strcpy(fc_fkattname, "fk.");
	fc_sep = "(";
	for (fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		Oid			fc_pk_type = RIAttType(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
		Oid			fc_fk_type = RIAttType(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);
		Oid			fc_pk_coll = RIAttCollation(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]);
		Oid			fc_fk_coll = RIAttCollation(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]);

		fc_quoteOneName(fc_pkattname + 3,
					 RIAttName(fc_pk_rel, fc_riinfo->pk_attnums[fc_i]));
		fc_quoteOneName(fc_fkattname + 3,
					 RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		fc_ri_GenerateQual(&fc_querybuf, fc_sep,
						fc_pkattname, fc_pk_type,
						fc_riinfo->pf_eq_oprs[fc_i],
						fc_fkattname, fc_fk_type);
		if (fc_pk_coll != fc_fk_coll)
			fc_ri_GenerateQualCollation(&fc_querybuf, fc_pk_coll);
		fc_sep = "AND";
	}

	/*
	 * 用分区约束开始 WHERE 子句（除非这是默认分区且没有其他分区，因为在这种情况下分区约束是空字符串。）
	 */
	fc_constraintDef = pg_get_partconstrdef_string(RelationGetRelid(fc_pk_rel), "pk");
	if (fc_constraintDef && fc_constraintDef[0] != '\0')
		appendStringInfo(&fc_querybuf, ") WHERE %s AND (",
						 fc_constraintDef);
	else
		appendStringInfoString(&fc_querybuf, ") WHERE (");

	fc_sep = "";
	for (fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		fc_quoteOneName(fc_fkattname, RIAttName(fc_fk_rel, fc_riinfo->fk_attnums[fc_i]));
		appendStringInfo(&fc_querybuf,
						 "%sfk.%s IS NOT NULL",
						 fc_sep, fc_fkattname);
		switch (fc_riinfo->confmatchtype)
		{
			case FKCONSTR_MATCH_SIMPLE:
				fc_sep = " AND ";
				break;
			case FKCONSTR_MATCH_FULL:
				fc_sep = " OR ";
				break;
		}
	}
	appendStringInfoChar(&fc_querybuf, ')');

	/*
	 * 临时增加 work_mem，以便更有效地执行检查查询。
	 * 这样做似乎没有问题，因为查询足够简单，不会使用
	 * 多倍的 work_mem，而且通常不会有许多大的外键验证
	 * 同时进行。因此这似乎符合被视为“维护”
	 * 操作的标准，因此我们使用 maintenance_work_mem。 
	 * 但是，我们还必须将 hash_mem_multiplier 设置为 1，因为
	 * 让它应用于 maintenance_work_mem 值显然是不允许的。
	 *
	 * 我们使用等效于函数 SET 选项的方法，以允许设置在
	 * 检查查询的确切持续时间内保持。guc.c 也负责在错误
	 * 时撤销设置。
	 */
	fc_save_nestlevel = NewGUCNestLevel();

	snprintf(fc_workmembuf, sizeof(fc_workmembuf), "%d", maintenance_work_mem);
	(void) set_config_option("work_mem", fc_workmembuf,
							 PGC_USERSET, PGC_S_SESSION,
							 GUC_ACTION_SAVE, true, 0, false);
	(void) set_config_option("hash_mem_multiplier", "1",
							 PGC_USERSET, PGC_S_SESSION,
							 GUC_ACTION_SAVE, true, 0, false);

	if (SPI_connect() != SPI_OK_CONNECT)
		elog(ERROR, "SPI_connect failed");

	/*
	 * 生成计划。我们不需要缓存它，并且没有
	 * 传递给计划的参数。
	 */
	fc_qplan = SPI_prepare(fc_querybuf.data, 0, NULL);

	if (fc_qplan == NULL)
		elog(ERROR, "SPI_prepare returned %s for %s",
			 SPI_result_code_string(SPI_result), fc_querybuf.data);

	/*
	 * 执行计划。出于安全考虑，我们强制使用当前快照。
	 * （在事务快照模式下，这可以说违反了事务隔离
	 * 规则，但我们实际上没有太多选择。）我们不需要
	 * 注册快照，因为 SPI_execute_snapshot 会处理这个问题。
	 * 我们最多需要返回一个元组，因此传递 limit = 1。
	 */
	fc_spi_result = SPI_execute_snapshot(fc_qplan,
									  NULL, NULL,
									  GetLatestSnapshot(),
									  InvalidSnapshot,
									  true, false, 1);

	/* 检查结果 */
	if (fc_spi_result != SPI_OK_SELECT)
		elog(ERROR, "SPI_execute_snapshot returned %s", SPI_result_code_string(fc_spi_result));

	/* 我们是否找到了可能违反约束的元组？ */
	if (SPI_processed > 0)
	{
		TupleTableSlot *fc_slot;
		HeapTuple	fc_tuple = SPI_tuptable->vals[0];
		TupleDesc	fc_tupdesc = SPI_tuptable->tupdesc;
		RI_ConstraintInfo fc_fake_riinfo;

		fc_slot = MakeSingleTupleTableSlot(fc_tupdesc, &TTSOpsVirtual);

		heap_deform_tuple(fc_tuple, fc_tupdesc,
						  fc_slot->tts_values, fc_slot->tts_isnull);
		ExecStoreVirtualTuple(fc_slot);

		/*
		 * 在结果元组中查看的列是 1..N，而不是在 fk_rel 中的列。 修改 riinfo，以便 ri_ReportViolation 能够正常工作。
		 *
		 * 除此之外，我们必须将正确的 tupdesc 传递给 ri_ReportViolation，覆盖其使用 pk_rel 或 fk_rel 的 tupdesc 的正常习惯。
		 */
		memcpy(&fc_fake_riinfo, fc_riinfo, sizeof(RI_ConstraintInfo));
		for (fc_i = 0; fc_i < fc_fake_riinfo.nkeys; fc_i++)
			fc_fake_riinfo.pk_attnums[fc_i] = fc_i + 1;

		fc_ri_ReportViolation(&fc_fake_riinfo, fc_pk_rel, fc_fk_rel,
						   fc_slot, fc_tupdesc, 0, true);
	}

	if (SPI_finish() != SPI_OK_FINISH)
		elog(ERROR, "SPI_finish failed");

	/*
	 * 恢复 work_mem 和 hash_mem_multiplier。
	 */
	AtEOXact_GUC(true, fc_save_nestlevel);
}


/* ----------
 * 以下是本地函数
 * ----------
 */


/*
 * quoteOneName --- 安全地引号单个 SQL 名称
 *
 * buffer 必须是 MAX_QUOTED_NAME_LEN 长（包括 \0 的空间）
 */
static void fc_quoteOneName(char *fc_buffer, const char *fc_name)
{
	/* 与其尝试聪明点，不如总是给它加引号。 */
	*fc_buffer++ = '"';
	while (*fc_name)
	{
		if (*fc_name == '"')
			*fc_buffer++ = '"';
		*fc_buffer++ = *fc_name++;
	}
	*fc_buffer++ = '"';
	*fc_buffer = '\0';
}

/*
 * quoteRelationName --- 安全地引号完全限定的关系名称
 *
 * buffer 必须是 MAX_QUOTED_REL_NAME_LEN 长（包括 \0 的空间）
 */
static void fc_quoteRelationName(char *fc_buffer, Relation fc_rel)
{
	fc_quoteOneName(fc_buffer, get_namespace_name(RelationGetNamespace(fc_rel)));
	fc_buffer += strlen(fc_buffer);
	*fc_buffer++ = '.';
	fc_quoteOneName(fc_buffer, RelationGetRelationName(fc_rel));
}

/*
 * ri_GenerateQual --- 生成一个等于两个变量的 WHERE 子句
 *
 * 这基本上将 " sep leftop op rightop" 附加到 buf，根据需要添加类型转换和模式限定，以确保解析器选择我们指定的运算符。 如果 leftop 和 rightop 不是变量或参数，则应加括号。
 */
static void fc_ri_GenerateQual(StringInfo fc_buf,
				const char *fc_sep,
				const char *fc_leftop, Oid fc_leftoptype,
				Oid fc_opoid,
				const char *fc_rightop, Oid fc_rightoptype)
{
	appendStringInfo(fc_buf, " %s ", fc_sep);
	generate_operator_clause(fc_buf, fc_leftop, fc_leftoptype, fc_opoid,
							 fc_rightop, fc_rightoptype);
}

/*
 * ri_GenerateQualCollation --- 将 COLLATE 规范添加到 WHERE 子句
 *
 * 目前，我们故意不将此函数用于将变量与 $n 参数进行比较的 RI 查询。 由于参数符号总是具有默认排序规则，因此效果将是使用变量的排序规则。 这是在测试引用列时唯一严格正确的，因为 SQL 标准规定 RI 比较应使用引用列的排序规则。 然而，只要所有排序规则都有相同的相等观念（它们确实如此，因为 texteq 简化为逐位相等），使用引用列的排序规则进行测试不会有可见的语义影响，这是好事，因为它允许我们在引用列上使用常规索引。 但是，如果直接比较引用列和被引用列，它们的排序规则不同，则必须使用此函数；否则解析器将无法解析要使用的排序规则。
 */
static void fc_ri_GenerateQualCollation(StringInfo fc_buf, Oid fc_collation)
{
	HeapTuple	fc_tp;
	Form_pg_collation fc_colltup;
	char	   *fc_collname;
	char		fc_onename[MAX_QUOTED_NAME_LEN];

	/* 如果是不可排序的数据类型，则无需执行任何操作 */
	if (!OidIsValid(fc_collation))
		return;

	fc_tp = SearchSysCache1(COLLOID, ObjectIdGetDatum(fc_collation));
	if (!HeapTupleIsValid(fc_tp))
		elog(ERROR, "cache lookup failed for collation %u", fc_collation);
	fc_colltup = (Form_pg_collation) GETSTRUCT(fc_tp);
	fc_collname = NameStr(fc_colltup->collname);

	/*
	 * 我们总是为名称加上限定，出于简单和确保查询不是搜索路径依赖的原因。
	 */
	fc_quoteOneName(fc_onename, get_namespace_name(fc_colltup->collnamespace));
	appendStringInfo(fc_buf, " COLLATE %s", fc_onename);
	fc_quoteOneName(fc_onename, fc_collname);
	appendStringInfo(fc_buf, ".%s", fc_onename);

	ReleaseSysCache(fc_tp);
}

/* ----------
 * ri_BuildQueryKey -
 *
 * 为外键约束的已准备 SPI 计划构建哈希表键。
 *
 *		key: 输出参数，*key 根据其他参数填充
 *		riinfo: 从 pg_constraint 条目派生的信息
 *		constr_queryno: 一个标识查询类型的内部编号
 *			(见文件开头的 RI_PLAN_XXX 常量)
 * ----------
 */
static void fc_ri_BuildQueryKey(RI_QueryKey *fc_key, const RI_ConstraintInfo *fc_riinfo,
				 int32 fc_constr_queryno)
{
	/*
	 * 具有共同祖先的继承约束可以共享 ri_query_cache
	 * 对于所有查询类型，除了 RI_PLAN_CHECK_LOOKUPPK_FROM_PK。
	 * 除了在那种情况下，该查询处理与外键约束相关的其他表
	 * (即，不是触发器已经被触发的表)，因此在继承树的所有成员
	 * 中将是相同的。因此，我们可以在哈希键中使用根约束的 OID，而
	 * 不是约束自己的 OID。这避免了创建重复的 SPI
	 * 计划，在存在许多具有相似外键约束的分区时节省了很多工作和内存。
	 *
	 * (注意，我们仍然必须为每个约束拥有单独的 RI_ConstraintInfo，因为
	 * 分区可以具有不同的列顺序，导致不同的 pk_attnums[] 或 fk_attnums[]
	 * 数组内容。)
	 *
	 * 我们假设结构 RI_QueryKey 不包含填充字节，否则我们需要
	 * 使用 memset 来清除它们。
	 */
	if (fc_constr_queryno != RI_PLAN_CHECK_LOOKUPPK_FROM_PK)
		fc_key->constr_id = fc_riinfo->constraint_root_id;
	else
		fc_key->constr_id = fc_riinfo->constraint_id;
	fc_key->constr_queryno = fc_constr_queryno;
}

/*
 * 检查 RI 触发器函数是否在预期上下文中被调用
 */
static void fc_ri_CheckTrigger(FunctionCallInfo fcinfo, const char *fc_funcname, int fc_tgkind)
{
	TriggerData *fc_trigdata = (TriggerData *) fcinfo->context;

	if (!CALLED_AS_TRIGGER(fcinfo))
		ereport(ERROR,
				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
				 errmsg("function \"%s\" was not called by trigger manager", fc_funcname)));

	/*
	 * 检查适当的事件
	 */
	if (!TRIGGER_FIRED_AFTER(fc_trigdata->tg_event) ||
		!TRIGGER_FIRED_FOR_ROW(fc_trigdata->tg_event))
		ereport(ERROR,
				(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
				 errmsg("function \"%s\" must be fired AFTER ROW", fc_funcname)));

	switch (fc_tgkind)
	{
		case RI_TRIGTYPE_INSERT:
			if (!TRIGGER_FIRED_BY_INSERT(fc_trigdata->tg_event))
				ereport(ERROR,
						(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
						 errmsg("function \"%s\" must be fired for INSERT", fc_funcname)));
			break;
		case RI_TRIGTYPE_UPDATE:
			if (!TRIGGER_FIRED_BY_UPDATE(fc_trigdata->tg_event))
				ereport(ERROR,
						(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
						 errmsg("function \"%s\" must be fired for UPDATE", fc_funcname)));
			break;
		case RI_TRIGTYPE_DELETE:
			if (!TRIGGER_FIRED_BY_DELETE(fc_trigdata->tg_event))
				ereport(ERROR,
						(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
						 errmsg("function \"%s\" must be fired for DELETE", fc_funcname)));
			break;
	}
}


/*
 * 获取触发器的外键约束的 RI_ConstraintInfo 结构。
 */
static const RI_ConstraintInfo *
fc_ri_FetchConstraintInfo(Trigger *fc_trigger, Relation fc_trig_rel, bool fc_rel_is_pk)
{
	Oid			fc_constraintOid = fc_trigger->tgconstraint;
	const RI_ConstraintInfo *fc_riinfo;

	/*
	 * 检查外键约束的 OID 是否可用；如果我们是通过普通触发器或
	 * 旧式的“约束触发器”被调用，则可能不可用。
	 */
	if (!OidIsValid(fc_constraintOid))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
				 errmsg("no pg_constraint entry for trigger \"%s\" on table \"%s\"",
						fc_trigger->tgname, RelationGetRelationName(fc_trig_rel)),
				 errhint("Remove this referential integrity trigger and its mates, then do ALTER TABLE ADD CONSTRAINT.")));

	/* 查找或创建约束的哈希表条目 */
	fc_riinfo = fc_ri_LoadConstraintInfo(fc_constraintOid);

	/* 对触发器调用数据进行一些简单的交叉检查 */
	if (fc_rel_is_pk)
	{
		if (fc_riinfo->fk_relid != fc_trigger->tgconstrrelid ||
			fc_riinfo->pk_relid != RelationGetRelid(fc_trig_rel))
			elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",
				 fc_trigger->tgname, RelationGetRelationName(fc_trig_rel));
	}
	else
	{
		if (fc_riinfo->fk_relid != RelationGetRelid(fc_trig_rel) ||
			fc_riinfo->pk_relid != fc_trigger->tgconstrrelid)
			elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",
				 fc_trigger->tgname, RelationGetRelationName(fc_trig_rel));
	}

	if (fc_riinfo->confmatchtype != FKCONSTR_MATCH_FULL &&
		fc_riinfo->confmatchtype != FKCONSTR_MATCH_PARTIAL &&
		fc_riinfo->confmatchtype != FKCONSTR_MATCH_SIMPLE)
		elog(ERROR, "unrecognized confmatchtype: %d",
			 fc_riinfo->confmatchtype);

	if (fc_riinfo->confmatchtype == FKCONSTR_MATCH_PARTIAL)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("MATCH PARTIAL not yet implemented")));

	return fc_riinfo;
}

/*
 * 获取或创建外键约束的 RI_ConstraintInfo 结构。
 */
static const RI_ConstraintInfo *
fc_ri_LoadConstraintInfo(Oid fc_constraintOid)
{
	RI_ConstraintInfo *fc_riinfo;
	bool		fc_found;
	HeapTuple	fc_tup;
	Form_pg_constraint fc_conForm;

	/*
	 * 在第一次调用时初始化哈希表
	 */
	if (!ri_constraint_cache)
		fc_ri_InitHashTables();

	/*
	 * 查找或创建一个哈希条目。如果找到有效条目，则直接返回。
	 */
	fc_riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache,
											   (void *) &fc_constraintOid,
											   HASH_ENTER, &fc_found);
	if (!fc_found)
		fc_riinfo->valid = false;
	else if (fc_riinfo->valid)
		return fc_riinfo;

	/*
	 * 获取 pg_constraint 行，以便我们可以填写条目。
	 */
	fc_tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fc_constraintOid));
	if (!HeapTupleIsValid(fc_tup)) /* 不应该发生 */
		elog(ERROR, "cache lookup failed for constraint %u", fc_constraintOid);
	fc_conForm = (Form_pg_constraint) GETSTRUCT(fc_tup);

	if (fc_conForm->contype != CONSTRAINT_FOREIGN) /* 不应该发生 */
		elog(ERROR, "constraint %u is not a foreign key constraint",
			 fc_constraintOid);

	/* 并提取数据 */
	Assert(fc_riinfo->constraint_id == fc_constraintOid);
	if (OidIsValid(fc_conForm->conparentid))
		fc_riinfo->constraint_root_id =
			get_ri_constraint_root(fc_conForm->conparentid);
	else
		fc_riinfo->constraint_root_id = fc_constraintOid;
	fc_riinfo->oidHashValue = GetSysCacheHashValue1(CONSTROID,
												 ObjectIdGetDatum(fc_constraintOid));
	fc_riinfo->rootHashValue = GetSysCacheHashValue1(CONSTROID,
												  ObjectIdGetDatum(fc_riinfo->constraint_root_id));
	memcpy(&fc_riinfo->conname, &fc_conForm->conname, sizeof(NameData));
	fc_riinfo->pk_relid = fc_conForm->confrelid;
	fc_riinfo->fk_relid = fc_conForm->conrelid;
	fc_riinfo->confupdtype = fc_conForm->confupdtype;
	fc_riinfo->confdeltype = fc_conForm->confdeltype;
	fc_riinfo->confmatchtype = fc_conForm->confmatchtype;

	DeconstructFkConstraintRow(fc_tup,
							   &fc_riinfo->nkeys,
							   fc_riinfo->fk_attnums,
							   fc_riinfo->pk_attnums,
							   fc_riinfo->pf_eq_oprs,
							   fc_riinfo->pp_eq_oprs,
							   fc_riinfo->ff_eq_oprs,
							   &fc_riinfo->ndelsetcols,
							   fc_riinfo->confdelsetcols);

	ReleaseSysCache(fc_tup);

	/*
	 * 为了高效处理下面的失效消息，我们保持一个
	 * 双向链表，以及一个当前有效条目的计数。
	 */
	dlist_push_tail(&ri_constraint_cache_valid_list, &fc_riinfo->valid_link);
	ri_constraint_cache_valid_count++;

	fc_riinfo->valid = true;

	return fc_riinfo;
}

/*
 * get_ri_constraint_root
 *		返回约束根父项的 OID
 */
static Oid get_ri_constraint_root(Oid fc_constrOid)
{
	for (;;)
	{
		HeapTuple	fc_tuple;
		Oid			fc_constrParentOid;

		fc_tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fc_constrOid));
		if (!HeapTupleIsValid(fc_tuple))
			elog(ERROR, "cache lookup failed for constraint %u", fc_constrOid);
		fc_constrParentOid = ((Form_pg_constraint) GETSTRUCT(fc_tuple))->conparentid;
		ReleaseSysCache(fc_tuple);
		if (!OidIsValid(fc_constrParentOid))
			break;				/* 我们已经到达根约束 */
		fc_constrOid = fc_constrParentOid;
	}
	return fc_constrOid;
}

/*
 * pg_constraint 失效事件的回调
 *
 * 虽然大多数系统缓存回调只是刷新所有条目，但 pg_constraint
 * 收到的更新流量足够多，这值得更加智能处理。
 * 使与具有指定哈希值的 syscache 条目关联的任何 ri_constraint_cache 条目失效，
 * 或者如果 hashvalue == 0，则使所有条目失效。
 *
 * 注意：在处理缓存失效消息时，可能存在对缓存的活动引用。
 * 因此，我们从不从缓存中删除条目，而只是将其标记为无效，这对活动使用是无害的。
 * （使用条目的任何查询都应该持有足够的锁，以防止其数据在它之下更改——但我们可能仍会出现缓存刷新。）
 */
static void fc_InvalidateConstraintCacheCallBack(Datum fc_arg, int fc_cacheid, uint32 fc_hashvalue)
{
	dlist_mutable_iter fc_iter;

	Assert(ri_constraint_cache != NULL);

	/*
	 * 如果当前有效条目的列表过于庞大，我们将所有条目标记为无效
	 * 以便可以清空列表。这种安排避免了在会话触及许多外键
	 * 并且还进行许多 ALTER TABLE 的情况下出现 O(N^2) 的行为，例如从 pg_dump 恢复。
	 */
	if (ri_constraint_cache_valid_count > 1000)
		fc_hashvalue = 0;			/* 假装这是一个缓存重置 */

	dlist_foreach_modify(fc_iter, &ri_constraint_cache_valid_list)
	{
		RI_ConstraintInfo *fc_riinfo = dlist_container(RI_ConstraintInfo,
													valid_link, fc_iter.cur);

		/*
		 * 我们必须使不仅直接匹配给定
		 * 哈希值的条目无效，还要使子条目无效，以防失效
		 * 影响根约束。
		 */
		if (fc_hashvalue == 0 ||
			fc_riinfo->oidHashValue == fc_hashvalue ||
			fc_riinfo->rootHashValue == fc_hashvalue)
		{
			fc_riinfo->valid = false;
			/* 也从列表中移除失效的条目 */
			dlist_delete(fc_iter.cur);
			ri_constraint_cache_valid_count--;
		}
	}
}


/*
 * 为执行查询以强制执行 RI 约束准备执行计划
 */
static SPIPlanPtr fc_ri_PlanCheck(const char *fc_querystr, int fc_nargs, Oid *fc_argtypes,
			 RI_QueryKey *fc_qkey, Relation fc_fk_rel, Relation fc_pk_rel)
{
	SPIPlanPtr	fc_qplan;
	Relation	fc_query_rel;
	Oid			fc_save_userid;
	int			fc_save_sec_context;

	/*
	 * 使用查询类型代码确定查询是针对
	 * PK 还是 FK 表进行的；我们将作为该表的拥有者进行检查
	 */
	if (fc_qkey->constr_queryno <= RI_PLAN_LAST_ON_PK)
		fc_query_rel = fc_pk_rel;
	else
		fc_query_rel = fc_fk_rel;

	/* 切换到适当的 UID 进行检查 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(RelationGetForm(fc_query_rel)->relowner,
						   fc_save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
						   SECURITY_NOFORCE_RLS);

	/* 创建计划 */
	fc_qplan = SPI_prepare(fc_querystr, fc_nargs, fc_argtypes);

	if (fc_qplan == NULL)
		elog(ERROR, "SPI_prepare returned %s for %s", SPI_result_code_string(SPI_result), fc_querystr);

	/* 恢复 UID 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	/* 保存计划 */
	SPI_keepplan(fc_qplan);
	fc_ri_HashPreparedPlan(fc_qkey, fc_qplan);

	return fc_qplan;
}

/*
 * 执行查询以强制执行 RI 约束
 */
static bool fc_ri_PerformCheck(const RI_ConstraintInfo *fc_riinfo,
				RI_QueryKey *fc_qkey, SPIPlanPtr fc_qplan,
				Relation fc_fk_rel, Relation fc_pk_rel,
				TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot,
				bool fc_detectNewRows, int fc_expect_OK)
{
	Relation	fc_query_rel,
				fc_source_rel;
	bool		fc_source_is_pk;
	Snapshot	fc_test_snapshot;
	Snapshot	fc_crosscheck_snapshot;
	int			fc_limit;
	int			fc_spi_result;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	Datum		fc_vals[RI_MAX_NUMKEYS * 2];
	char		fc_nulls[RI_MAX_NUMKEYS * 2];

	/*
	 * 使用查询类型代码确定查询是针对
	 * PK 还是 FK 表进行的；我们将作为该表的拥有者进行检查
	 */
	if (fc_qkey->constr_queryno <= RI_PLAN_LAST_ON_PK)
		fc_query_rel = fc_pk_rel;
	else
		fc_query_rel = fc_fk_rel;

	/*
	 * 查询的值来自于调用触发器的表——它通常是相对于 query_rel 的另一个表。
	 * 一个例外是 ri_Check_Pk_Match()，它对两个表都使用 PK 表（并将
	 * queryno 设置为 RI_PLAN_CHECK_LOOKUPPK_FROM_PK）。我们最终可能
	 * 需要某种不那么拗口的方式来确定这一点。
	 */
	if (fc_qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK)
	{
		fc_source_rel = fc_fk_rel;
		fc_source_is_pk = false;
	}
	else
	{
		fc_source_rel = fc_pk_rel;
		fc_source_is_pk = true;
	}

	/* 提取要传入查询的参数 */
	if (fc_newslot)
	{
		fc_ri_ExtractValues(fc_source_rel, fc_newslot, fc_riinfo, fc_source_is_pk,
						 fc_vals, fc_nulls);
		if (fc_oldslot)
			fc_ri_ExtractValues(fc_source_rel, fc_oldslot, fc_riinfo, fc_source_is_pk,
							 fc_vals + fc_riinfo->nkeys, fc_nulls + fc_riinfo->nkeys);
	}
	else
	{
		fc_ri_ExtractValues(fc_source_rel, fc_oldslot, fc_riinfo, fc_source_is_pk,
						 fc_vals, fc_nulls);
	}

	/*
	 * 在 READ COMMITTED 模式下，我们只需使用最新的常规
	 * 快照，我们将看到所有可能有趣的行。但是在
	 * 事务快照模式下，我们不能更改事务快照。如果
	 * 调用方传递 detectNewRows == false，那么用
	 * 事务快照进行查询是可以的；否则我们使用当前快照，并
	 * 告诉执行者如果发现当前快照下的任何行在事务快照下不可见就报错。
	 * 注意 SPI_execute_snapshot 将注册快照，因此我们在这里不需要
	 * 费心处理。
	 */
	if (IsolationUsesXactSnapshot() && fc_detectNewRows)
	{
		CommandCounterIncrement();	/* 确保我自己的所有工作都是可见的 */
		fc_test_snapshot = GetLatestSnapshot();
		fc_crosscheck_snapshot = GetTransactionSnapshot();
	}
	else
	{
		/* 默认的 SPI 行为是可以的 */
		fc_test_snapshot = InvalidSnapshot;
		fc_crosscheck_snapshot = InvalidSnapshot;
	}

	/*
	 * 如果这是一个选择查询（例如，用于 'no action' 或 'restrict'
	 * 触发器），我们只需查看表中是否有单行
	 * 匹配该键。否则，limit = 0 - 因为我们希望查询
	 * 影响所有匹配的行。
	 */
	fc_limit = (fc_expect_OK == SPI_OK_SELECT) ? 1 : 0;

	/* 切换到适当的 UID 进行检查 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(RelationGetForm(fc_query_rel)->relowner,
						   fc_save_sec_context | SECURITY_LOCAL_USERID_CHANGE |
						   SECURITY_NOFORCE_RLS);

	/* 最后我们可以运行查询。 */
	fc_spi_result = SPI_execute_snapshot(fc_qplan,
									  fc_vals, fc_nulls,
									  fc_test_snapshot, fc_crosscheck_snapshot,
									  false, false, fc_limit);

	/* 恢复 UID 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	/* 检查结果 */
	if (fc_spi_result < 0)
		elog(ERROR, "SPI_execute_snapshot returned %s", SPI_result_code_string(fc_spi_result));

	if (fc_expect_OK >= 0 && fc_spi_result != fc_expect_OK)
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("referential integrity query on \"%s\" from constraint \"%s\" on \"%s\" gave unexpected result",
						RelationGetRelationName(fc_pk_rel),
						NameStr(fc_riinfo->conname),
						RelationGetRelationName(fc_fk_rel)),
				 errhint("This is most likely due to a rule having rewritten the query.")));

	/* XXX 这部分去调用方处理不是更清楚吗？ */
	if (fc_qkey->constr_queryno != RI_PLAN_CHECK_LOOKUPPK_FROM_PK &&
		fc_expect_OK == SPI_OK_SELECT &&
		(SPI_processed == 0) == (fc_qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK))
		fc_ri_ReportViolation(fc_riinfo,
						   fc_pk_rel, fc_fk_rel,
						   fc_newslot ? fc_newslot : fc_oldslot,
						   NULL,
						   fc_qkey->constr_queryno, false);

	return SPI_processed != 0;
}

/*
 * 从元组中提取字段到 Datum/nulls 数组
 */
static void fc_ri_ExtractValues(Relation fc_rel, TupleTableSlot *fc_slot,
				 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk,
				 Datum *fc_vals, char *fc_nulls)
{
	const int16 *fc_attnums;
	bool		fc_isnull;

	if (fc_rel_is_pk)
		fc_attnums = fc_riinfo->pk_attnums;
	else
		fc_attnums = fc_riinfo->fk_attnums;

	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		fc_vals[fc_i] = slot_getattr(fc_slot, fc_attnums[fc_i], &fc_isnull);
		fc_nulls[fc_i] = fc_isnull ? 'n' : ' ';
	}
}

/*
 * 产生错误报告
 *
 * 如果失败的约束是在 FK 表的插入/更新上，
 * 我们希望从那里提取键名和值，错误
 * 消息看起来像 'key blah 在 PK 中不存在'。
 * 否则，属性名称和值来自于 PK 表，消息看起来像 'key blah 仍然被 FK 引用'。
 */
static void fc_ri_ReportViolation(const RI_ConstraintInfo *fc_riinfo,
				   Relation fc_pk_rel, Relation fc_fk_rel,
				   TupleTableSlot *fc_violatorslot, TupleDesc fc_tupdesc,
				   int fc_queryno, bool fc_partgone)
{
	StringInfoData fc_key_names;
	StringInfoData fc_key_values;
	bool		fc_onfk;
	const int16 *fc_attnums;
	Oid			fc_rel_oid;
	AclResult	fc_aclresult;
	bool		fc_has_perm = true;

	/*
	 * 确定要抱怨的关系。如果 tupdesc 不是由
	 * 调用者传递的，则假设违规元组来自那里。
	 */
	fc_onfk = (fc_queryno == RI_PLAN_CHECK_LOOKUPPK);
	if (fc_onfk)
	{
		fc_attnums = fc_riinfo->fk_attnums;
		fc_rel_oid = fc_fk_rel->rd_id;
		if (fc_tupdesc == NULL)
			fc_tupdesc = fc_fk_rel->rd_att;
	}
	else
	{
		fc_attnums = fc_riinfo->pk_attnums;
		fc_rel_oid = fc_pk_rel->rd_id;
		if (fc_tupdesc == NULL)
			fc_tupdesc = fc_pk_rel->rd_att;
	}

	/*
	 * 检查权限——如果用户没有访问
	 * 任何键列的数据的权限，则不包括下面的 errdetail()。
	 *
	 * 首先检查关系上的 RLS 是否启用。
	 * 如果是，我们不返回任何具体信息以避免泄漏数据。
	 *
	 * 接下来检查表级权限，如果未通过，
	 * 则检查列级特权。
	 *
	 * 当被引用的侧的分区被分离/删除时，
	 * 我们不需要检查，因为用户必须是表的拥有者。
	 */
	if (fc_partgone)
		fc_has_perm = true;
	else if (check_enable_rls(fc_rel_oid, InvalidOid, true) != RLS_ENABLED)
	{
		fc_aclresult = pg_class_aclcheck(fc_rel_oid, GetUserId(), ACL_SELECT);
		if (fc_aclresult != ACLCHECK_OK)
		{
			/****** 尝试进行列级权限 ******/
			for (int fc_idx = 0; fc_idx < fc_riinfo->nkeys; fc_idx++)
			{
				fc_aclresult = pg_attribute_aclcheck(fc_rel_oid, fc_attnums[fc_idx],
												  GetUserId(),
												  ACL_SELECT);

				/****** 无访问权限 ******/
				if (fc_aclresult != ACLCHECK_OK)
				{
					fc_has_perm = false;
					break;
				}
			}
		}
	}
	else
		fc_has_perm = false;

	if (fc_has_perm)
	{
		/* 获取涉及的键的可打印版本 */
		initStringInfo(&fc_key_names);
		initStringInfo(&fc_key_values);
		for (int fc_idx = 0; fc_idx < fc_riinfo->nkeys; fc_idx++)
		{
			int			fc_fnum = fc_attnums[fc_idx];
			Form_pg_attribute fc_att = TupleDescAttr(fc_tupdesc, fc_fnum - 1);
			char	   *fc_name,
					   *fc_val;
			Datum		fc_datum;
			bool		fc_isnull;

			fc_name = NameStr(fc_att->attname);

			fc_datum = slot_getattr(fc_violatorslot, fc_fnum, &fc_isnull);
			if (!fc_isnull)
			{
				Oid			fc_foutoid;
				bool		fc_typisvarlena;

				getTypeOutputInfo(fc_att->atttypid, &fc_foutoid, &fc_typisvarlena);
				fc_val = OidOutputFunctionCall(fc_foutoid, fc_datum);
			}
			else
				fc_val = "null";

			if (fc_idx > 0)
			{
				appendStringInfoString(&fc_key_names, ", ");
				appendStringInfoString(&fc_key_values, ", ");
			}
			appendStringInfoString(&fc_key_names, fc_name);
			appendStringInfoString(&fc_key_values, fc_val);
		}
	}

	if (fc_partgone)
		ereport(ERROR,
				(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
				 errmsg("removing partition \"%s\" violates foreign key constraint \"%s\"",
						RelationGetRelationName(fc_pk_rel),
						NameStr(fc_riinfo->conname)),
				 errdetail("Key (%s)=(%s) is still referenced from table \"%s\".",
						   fc_key_names.data, fc_key_values.data,
						   RelationGetRelationName(fc_fk_rel)),
				 errtableconstraint(fc_fk_rel, NameStr(fc_riinfo->conname))));
	else if (fc_onfk)
		ereport(ERROR,
				(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
				 errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
						RelationGetRelationName(fc_fk_rel),
						NameStr(fc_riinfo->conname)),
				 fc_has_perm ?
				 errdetail("Key (%s)=(%s) is not present in table \"%s\".",
						   fc_key_names.data, fc_key_values.data,
						   RelationGetRelationName(fc_pk_rel)) :
				 errdetail("Key is not present in table \"%s\".",
						   RelationGetRelationName(fc_pk_rel)),
				 errtableconstraint(fc_fk_rel, NameStr(fc_riinfo->conname))));
	else
		ereport(ERROR,
				(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
				 errmsg("update or delete on table \"%s\" violates foreign key constraint \"%s\" on table \"%s\"",
						RelationGetRelationName(fc_pk_rel),
						NameStr(fc_riinfo->conname),
						RelationGetRelationName(fc_fk_rel)),
				 fc_has_perm ?
				 errdetail("Key (%s)=(%s) is still referenced from table \"%s\".",
						   fc_key_names.data, fc_key_values.data,
						   RelationGetRelationName(fc_fk_rel)) :
				 errdetail("Key is still referenced from table \"%s\".",
						   RelationGetRelationName(fc_fk_rel)),
				 errtableconstraint(fc_fk_rel, NameStr(fc_riinfo->conname))));
}


/*
 * ri_NullCheck -
 *
 * 确定元组中所有键值的 NULL 状态
 *
 * 返回 RI_KEYS_ALL_NULL，RI_KEYS_NONE_NULL 或 RI_KEYS_SOME_NULL 之一。
 */
static int fc_ri_NullCheck(TupleDesc fc_tupDesc,
			 TupleTableSlot *fc_slot,
			 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk)
{
	const int16 *fc_attnums;
	bool		fc_allnull = true;
	bool		fc_nonenull = true;

	if (fc_rel_is_pk)
		fc_attnums = fc_riinfo->pk_attnums;
	else
		fc_attnums = fc_riinfo->fk_attnums;

	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		if (slot_attisnull(fc_slot, fc_attnums[fc_i]))
			fc_nonenull = false;
		else
			fc_allnull = false;
	}

	if (fc_allnull)
		return RI_KEYS_ALL_NULL;

	if (fc_nonenull)
		return RI_KEYS_NONE_NULL;

	return RI_KEYS_SOME_NULL;
}


/*
 * ri_InitHashTables -
 *
 * 初始化我们的内部哈希表。
 */
static void fc_ri_InitHashTables(void)
{
	HASHCTL		fc_ctl;

	fc_ctl.keysize = sizeof(Oid);
	fc_ctl.entrysize = sizeof(RI_ConstraintInfo);
	ri_constraint_cache = hash_create("RI constraint cache",
									  RI_INIT_CONSTRAINTHASHSIZE,
									  &fc_ctl, HASH_ELEM | HASH_BLOBS);

	/* 安排在 pg_constraint 变化时清除缓存 */
	CacheRegisterSyscacheCallback(CONSTROID,
								  fc_InvalidateConstraintCacheCallBack,
								  (Datum) 0);

	fc_ctl.keysize = sizeof(RI_QueryKey);
	fc_ctl.entrysize = sizeof(RI_QueryHashEntry);
	ri_query_cache = hash_create("RI query cache",
								 RI_INIT_QUERYHASHSIZE,
								 &fc_ctl, HASH_ELEM | HASH_BLOBS);

	fc_ctl.keysize = sizeof(RI_CompareKey);
	fc_ctl.entrysize = sizeof(RI_CompareHashEntry);
	ri_compare_cache = hash_create("RI compare cache",
								   RI_INIT_QUERYHASHSIZE,
								   &fc_ctl, HASH_ELEM | HASH_BLOBS);
}


/*
 * ri_FetchPreparedPlan -
 *
 * 在我们私有的已准备和保存的 SPI 执行计划哈希表中查找查询键。
 * 如果找到则返回计划，否则返回 NULL。
 */
static SPIPlanPtr fc_ri_FetchPreparedPlan(RI_QueryKey *fc_key)
{
	RI_QueryHashEntry *fc_entry;
	SPIPlanPtr	fc_plan;

	/*
	 * 在第一次调用时初始化哈希表
	 */
	if (!ri_query_cache)
		fc_ri_InitHashTables();

	/*
	 * 查找键
	 */
	fc_entry = (RI_QueryHashEntry *) hash_search(ri_query_cache,
											  (void *) fc_key,
											  HASH_FIND, NULL);
	if (fc_entry == NULL)
		return NULL;

	/*
	 * 检查计划是否仍然有效。如果没有，我们不想仅仅依赖 plancache.c
	 * 重新生成它；相反，我们应该从头开始并重建查询文本。这是为了
	 * 覆盖诸如表/列重命名等情况。我们依赖 plancache 机制来
	 * 检测可能的无效情况。
	 *
	 * 警告：此检查仅在调用者已经锁定外键和主键关系时才可信。
	 */
	fc_plan = fc_entry->plan;
	if (fc_plan && SPI_plan_is_valid(fc_plan))
		return fc_plan;

	/*
	 * 否则我们不妨现在清除缓存的计划，以在制作新计划前释放一点
	 * 内存空间。
	 */
	fc_entry->plan = NULL;
	if (fc_plan)
		SPI_freeplan(fc_plan);

	return NULL;
}


/*
 * ri_HashPreparedPlan -
 *
 * 将另一个计划添加到我们私有的 SPI 查询计划哈希表中。
 */
static void fc_ri_HashPreparedPlan(RI_QueryKey *fc_key, SPIPlanPtr fc_plan)
{
	RI_QueryHashEntry *fc_entry;
	bool		fc_found;

	/*
	 * 在第一次调用时初始化哈希表
	 */
	if (!ri_query_cache)
		fc_ri_InitHashTables();

	/*
	 * 添加新计划。我们可能会覆盖之前通过 ri_FetchPreparedPlan
	 * 找到的无效条目。
	 */
	fc_entry = (RI_QueryHashEntry *) hash_search(ri_query_cache,
											  (void *) fc_key,
											  HASH_ENTER, &fc_found);
	Assert(!fc_found || fc_entry->plan == NULL);
	fc_entry->plan = fc_plan;
}


/*
 * ri_KeysEqual -
 *
 * 检查 OLD 和 NEW 中的所有键值是否相等。
 *
 * 注意：在某个时候我们可能希望将其重新定义为检查
 * “IS NOT DISTINCT” 而不是 “=”，即，允许两个 NULL 被视为
 * 相等。目前没有必要，因为所有调用者之前至少发现其中一行不包含 NULL。
 */
static bool fc_ri_KeysEqual(Relation fc_rel, TupleTableSlot *fc_oldslot, TupleTableSlot *fc_newslot,
			 const RI_ConstraintInfo *fc_riinfo, bool fc_rel_is_pk)
{
	const int16 *fc_attnums;

	if (fc_rel_is_pk)
		fc_attnums = fc_riinfo->pk_attnums;
	else
		fc_attnums = fc_riinfo->fk_attnums;

	/* XXX：一次获取所有必要的属性可能是值得的 */
	for (int fc_i = 0; fc_i < fc_riinfo->nkeys; fc_i++)
	{
		Datum		fc_oldvalue;
		Datum		fc_newvalue;
		bool		fc_isnull;

		/*
		 * 获取一个属性的旧值。如果它为 NULL - 则它们不相等。
		 */
		fc_oldvalue = slot_getattr(fc_oldslot, fc_attnums[fc_i], &fc_isnull);
		if (fc_isnull)
			return false;

		/*
		 * 获取一个属性的新值。如果为NULL，则它们不相等。
		 */
		fc_newvalue = slot_getattr(fc_newslot, fc_attnums[fc_i], &fc_isnull);
		if (fc_isnull)
			return false;

		if (fc_rel_is_pk)
		{
			/*
			 * 如果我们正在查看主键表，进行逐字节比较。
			 * 如果值被更改为一个“看起来”不同但使用相等操作符进行比较时会相等的值，我们必须传播主键变化。
			 * 这仅对 ON UPDATE CASCADE 有影响，但为了保持一致性，我们将所有对主键的更改视为相同。
			 */
			Form_pg_attribute fc_att = TupleDescAttr(fc_oldslot->tts_tupleDescriptor, fc_attnums[fc_i] - 1);

			if (!datum_image_eq(fc_oldvalue, fc_newvalue, fc_att->attbyval, fc_att->attlen))
				return false;
		}
		else
		{
			/*
			 * 对于外键表，使用适当的相等操作符进行比较。
			 * 比较相等的更改在更新后仍然会满足约束。
			 */
			if (!fc_ri_AttributesEqual(fc_riinfo->ff_eq_oprs[fc_i], RIAttType(fc_rel, fc_attnums[fc_i]),
									fc_oldvalue, fc_newvalue))
				return false;
		}
	}

	return true;
}


/*
 * ri_AttributesEqual -
 *
 * 调用适当的相等比较操作符来比较两个值。
 *
 * 注意：我们已经检查过这两个值都不是 null。
 */
static bool fc_ri_AttributesEqual(Oid fc_eq_opr, Oid fc_typeid,
				   Datum fc_oldvalue, Datum fc_newvalue)
{
	RI_CompareHashEntry *fc_entry = fc_ri_HashCompareOp(fc_eq_opr, fc_typeid);

	/* 我们需要对值进行类型转换吗？ */
	if (OidIsValid(fc_entry->cast_func_finfo.fn_oid))
	{
		fc_oldvalue = FunctionCall3(&fc_entry->cast_func_finfo,
								 fc_oldvalue,
								 Int32GetDatum(-1), /* typmod */
								 BoolGetDatum(false));	/* 隐式强制转换 */
		fc_newvalue = FunctionCall3(&fc_entry->cast_func_finfo,
								 fc_newvalue,
								 Int32GetDatum(-1), /* typmod */
								 BoolGetDatum(false));	/* 隐式强制转换 */
	}

	/*
	 * 应用比较操作符。
	 *
	 * 注意：此函数是一个调用栈的一部分，该调用栈确定对行的更新是否重要到需要在外键约束的另一侧进行检查或采取行动。
	 * 因此，这里的比较需要使用*另一个*表的排序规则进行。
	 * 为了简单起见（例如，我们甚至可能没有打开另一个表），我们将在这里使用默认排序规则，这可能导致一些假阴性。
	 * 如果我们允许数据库范围的排序规则是非确定性的，所有这些都将失效。
	 */
	return DatumGetBool(FunctionCall2Coll(&fc_entry->eq_opr_finfo,
										  DEFAULT_COLLATION_OID,
										  fc_oldvalue, fc_newvalue));
}

/*
 * ri_HashCompareOp -
 *
 * 查看我们是否知道如何比较两个值，如果不知道，则创建一个新的哈希条目。
 */
static RI_CompareHashEntry *
fc_ri_HashCompareOp(Oid fc_eq_opr, Oid fc_typeid)
{
	RI_CompareKey fc_key;
	RI_CompareHashEntry *fc_entry;
	bool		fc_found;

	/*
	 * 在第一次调用时初始化哈希表
	 */
	if (!ri_compare_cache)
		fc_ri_InitHashTables();

	/*
	 * 找到或创建一个哈希条目。 注意我们假设 RI_CompareKey
	 * 不包含结构填充。
	 */
	fc_key.eq_opr = fc_eq_opr;
	fc_key.typeid = fc_typeid;
	fc_entry = (RI_CompareHashEntry *) hash_search(ri_compare_cache,
												(void *) &fc_key,
												HASH_ENTER, &fc_found);
	if (!fc_found)
		fc_entry->valid = false;

	/*
	 * 如果尚未初始化，则进行初始化。由于我们将在后端的生命周期内保持此哈希条目，
	 * 因此将函数缓存结构的任何相关信息放入 TopMemoryContext。
	 */
	if (!fc_entry->valid)
	{
		Oid			fc_lefttype,
					fc_righttype,
					fc_castfunc;
		CoercionPathType fc_pathtype;

		/* 我们始终需要知道如何调用相等操作符 */
		fmgr_info_cxt(get_opcode(fc_eq_opr), &fc_entry->eq_opr_finfo,
					  TopMemoryContext);

		/*
		 * 如果我们选择从外键类型转换为主键类型，我们可能需要应用强制转换函数以达到操作符的输入类型。
		 *
		 * XXX 最终支持数组强制转换案例在这里和 ri_AttributesEqual() 中将会很好。
		 * 目前没有意义，因为涉及非相同数组类型的案例将在约束创建时被拒绝。
		 *
		 * XXX 可能还考虑支持通过 IO 强制转换？ 目前没有必要，因为这不会为隐式强制转换生成。
		 */
		op_input_types(fc_eq_opr, &fc_lefttype, &fc_righttype);
		Assert(fc_lefttype == fc_righttype);
		if (fc_typeid == fc_lefttype)
			fc_castfunc = InvalidOid;	/* 最简单的情况 */
		else
		{
			fc_pathtype = find_coercion_pathway(fc_lefttype, fc_typeid,
											 COERCION_IMPLICIT,
											 &fc_castfunc);
			if (fc_pathtype != COERCION_PATH_FUNC &&
				fc_pathtype != COERCION_PATH_RELABELTYPE)
			{
				/*
				 * eq_opr 宣告的输入类型可能是多态类型，如 ANYARRAY 或 ANYENUM，或其他特殊情况如 RECORD；
				 * find_coercion_pathway 目前不包含这些特殊情况。
				 */
				if (!IsBinaryCoercible(fc_typeid, fc_lefttype))
					elog(ERROR, "no conversion function from %s to %s",
						 format_type_be(fc_typeid),
						 format_type_be(fc_lefttype));
			}
		}
		if (OidIsValid(fc_castfunc))
			fmgr_info_cxt(fc_castfunc, &fc_entry->cast_func_finfo,
						  TopMemoryContext);
		else
			fc_entry->cast_func_finfo.fn_oid = InvalidOid;
		fc_entry->valid = true;
	}

	return fc_entry;
}


/*
 * 给定一个触发器函数 OID，确定它是否是 RI 触发器，
 * 如果是的话，判断它是附加到主键还是外键关系。
 */
int RI_FKey_trigger_type(Oid fc_tgfoid)
{
	switch (fc_tgfoid)
	{
		case F_RI_FKEY_CASCADE_DEL:
		case F_RI_FKEY_CASCADE_UPD:
		case F_RI_FKEY_RESTRICT_DEL:
		case F_RI_FKEY_RESTRICT_UPD:
		case F_RI_FKEY_SETNULL_DEL:
		case F_RI_FKEY_SETNULL_UPD:
		case F_RI_FKEY_SETDEFAULT_DEL:
		case F_RI_FKEY_SETDEFAULT_UPD:
		case F_RI_FKEY_NOACTION_DEL:
		case F_RI_FKEY_NOACTION_UPD:
			return RI_TRIGGER_PK;

		case F_RI_FKEY_CHECK_INS:
		case F_RI_FKEY_CHECK_UPD:
			return RI_TRIGGER_FK;
	}

	return RI_TRIGGER_NONE;
}
